From 86ab9a2afc6de4e51301ee3dc278a17e7ea06413 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Thu, 21 Jan 2016 19:08:38 -0500 Subject: [PATCH 001/331] Make ssl includes be an array of directives instead of an include of a file. This will be the first step to filter out the things that would conflict with earlier declarations --- .../letsencrypt_nginx/configurator.py | 3 +-- letsencrypt-nginx/letsencrypt_nginx/parser.py | 18 +++++++++++++++++- .../tests/configurator_test.py | 13 +++++++------ .../letsencrypt_nginx/tests/parser_test.py | 11 +++++++++++ .../letsencrypt_nginx/tls_sni_01.py | 4 ++-- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index efa7e08b4..918ba53c2 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -332,8 +332,7 @@ 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_certificate_key', snakeoil_key]] + self.parser.loc["ssl_options"] self.parser.add_server_directives( vhost.filep, vhost.names, ssl_block, replace=False) vhost.ssl = True diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 3b1dd049e..935bf40dd 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -169,6 +169,18 @@ class NginxParser(object): logger.debug("Could not parse file: %s", item) return trees + def _parse_ssl_options(self, ssl_options): + if ssl_options is not None: + try: + with open(ssl_options) as _file: + return nginxparser.load(_file) + except IOError: + logger.debug("Could not open file: %s", item) + except pyparsing.ParseException: + logger.debug("Could not parse file: %s", item) + else: + return [] + def _set_locations(self, ssl_options): """Set default location for directives. @@ -188,7 +200,7 @@ class NginxParser(object): name = default return {"root": root, "default": default, "listen": listen, - "name": name, "ssl_options": ssl_options} + "name": name, "ssl_options": self._parse_ssl_options(ssl_options)} def _find_config_root(self): """Find the Nginx Configuration Root file.""" @@ -503,6 +515,10 @@ def _add_directive(block, directive, replace): See _add_directives for more documentation. """ + if directive[0] == '#': + 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. diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index 4fce33079..f35dcdfc0 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -216,9 +216,9 @@ class NginxConfiguratorTest(util.NginxTest): ['listen', '5001 ssl'], ['ssl_certificate', 'example/fullchain.pem'], - ['ssl_certificate_key', 'example/key.pem'], - ['include', self.config.parser.loc["ssl_options"]] - ]]], + ['ssl_certificate_key', 'example/key.pem']] + + util.filter_comments(self.config.parser.loc["ssl_options"]) + ]], parsed_example_conf) self.assertEqual([['server_name', 'somename alias another.alias']], parsed_server_conf) @@ -234,8 +234,9 @@ class NginxConfiguratorTest(util.NginxTest): ['index', 'index.html index.htm']]], ['listen', '5001 ssl'], ['ssl_certificate', '/etc/nginx/fullchain.pem'], - ['ssl_certificate_key', '/etc/nginx/key.pem'], - ['include', self.config.parser.loc["ssl_options"]]]], + ['ssl_certificate_key', '/etc/nginx/key.pem']]+ + util.filter_comments(self.config.parser.loc["ssl_options"]) + ], 2)) def test_get_all_certs_keys(self): @@ -394,6 +395,6 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index b64f1dee3..1190a2326 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -249,5 +249,16 @@ class NginxParserTest(util.NginxTest): ]) self.assertTrue(server['ssl']) + def test_ssl_options_should_be_parsed_ssl_directives(self): + nparser = parser.NginxParser(self.config_path, self.ssl_options) + self.assertEqual(nparser.loc["ssl_options"], + [['ssl_session_cache', 'shared:SSL:1m'], + ['ssl_session_timeout', '1440m'], + ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], + ['ssl_prefer_server_ciphers', 'on'], + ['#', ' Using list of ciphers from "Bulletproof SSL and TLS"'], + ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] + ]) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py index e59281c4c..5afc950ad 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py @@ -146,7 +146,6 @@ class NginxTlsSni01(common.TLSSNI01): block.extend([['server_name', achall.response(achall.account_key).z_domain], - ['include', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) ['access_log', os.path.join( @@ -155,6 +154,7 @@ class NginxTlsSni01(common.TLSSNI01): 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]]]]) + [['location', '/'], [['root', document_root]]]] + + self.configurator.parser.loc["ssl_options"]) return [['server'], block] From 25674b4d559a630e15ac2941e410bc4ca04586e6 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 12:12:29 -0500 Subject: [PATCH 002/331] Change name of session cache in order to minimize risk of conflict --- letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf b/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf index f0081c1fc..3faab8818 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf +++ b/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf @@ -1,4 +1,4 @@ -ssl_session_cache shared:SSL:1m; +ssl_session_cache shared:le_nginx_SSL:1m; ssl_session_timeout 1440m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; From d53da41f007e7f8750a528554bf4000b3d7c6a03 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 15:03:51 -0500 Subject: [PATCH 003/331] Stupid mistake, forgot to change the test --- letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index 1190a2326..3fc6a214e 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -252,7 +252,7 @@ class NginxParserTest(util.NginxTest): def test_ssl_options_should_be_parsed_ssl_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) self.assertEqual(nparser.loc["ssl_options"], - [['ssl_session_cache', 'shared:SSL:1m'], + [['ssl_session_cache', 'shared:le_nginx_SSL:1m'], ['ssl_session_timeout', '1440m'], ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], ['ssl_prefer_server_ciphers', 'on'], From 593e220353f720282e112809fddd954e575c6779 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 16:28:05 -0500 Subject: [PATCH 004/331] A final small fix, hopefully --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 935bf40dd..bd9bebe08 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -175,11 +175,10 @@ class NginxParser(object): with open(ssl_options) as _file: return nginxparser.load(_file) except IOError: - logger.debug("Could not open file: %s", item) + logger.debug("Could not open file: %s", ssl_options) except pyparsing.ParseException: - logger.debug("Could not parse file: %s", item) - else: - return [] + logger.debug("Could not parse file: %s", ssl_options) + return [] def _set_locations(self, ssl_options): """Set default location for directives. From 578db8b8406382da21db6731f4c63dbbeb9a4b2d Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Thu, 21 Jan 2016 19:08:38 -0500 Subject: [PATCH 005/331] Make ssl includes be an array of directives instead of an include of a file. This will be the first step to filter out the things that would conflict with earlier declarations --- .../letsencrypt_nginx/configurator.py | 3 +-- letsencrypt-nginx/letsencrypt_nginx/parser.py | 18 +++++++++++++++++- .../tests/configurator_test.py | 13 +++++++------ .../letsencrypt_nginx/tests/parser_test.py | 11 +++++++++++ .../letsencrypt_nginx/tls_sni_01.py | 4 ++-- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index efa7e08b4..918ba53c2 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -332,8 +332,7 @@ 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_certificate_key', snakeoil_key]] + self.parser.loc["ssl_options"] self.parser.add_server_directives( vhost.filep, vhost.names, ssl_block, replace=False) vhost.ssl = True diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 3b1dd049e..935bf40dd 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -169,6 +169,18 @@ class NginxParser(object): logger.debug("Could not parse file: %s", item) return trees + def _parse_ssl_options(self, ssl_options): + if ssl_options is not None: + try: + with open(ssl_options) as _file: + return nginxparser.load(_file) + except IOError: + logger.debug("Could not open file: %s", item) + except pyparsing.ParseException: + logger.debug("Could not parse file: %s", item) + else: + return [] + def _set_locations(self, ssl_options): """Set default location for directives. @@ -188,7 +200,7 @@ class NginxParser(object): name = default return {"root": root, "default": default, "listen": listen, - "name": name, "ssl_options": ssl_options} + "name": name, "ssl_options": self._parse_ssl_options(ssl_options)} def _find_config_root(self): """Find the Nginx Configuration Root file.""" @@ -503,6 +515,10 @@ def _add_directive(block, directive, replace): See _add_directives for more documentation. """ + if directive[0] == '#': + 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. diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index 4fce33079..f35dcdfc0 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -216,9 +216,9 @@ class NginxConfiguratorTest(util.NginxTest): ['listen', '5001 ssl'], ['ssl_certificate', 'example/fullchain.pem'], - ['ssl_certificate_key', 'example/key.pem'], - ['include', self.config.parser.loc["ssl_options"]] - ]]], + ['ssl_certificate_key', 'example/key.pem']] + + util.filter_comments(self.config.parser.loc["ssl_options"]) + ]], parsed_example_conf) self.assertEqual([['server_name', 'somename alias another.alias']], parsed_server_conf) @@ -234,8 +234,9 @@ class NginxConfiguratorTest(util.NginxTest): ['index', 'index.html index.htm']]], ['listen', '5001 ssl'], ['ssl_certificate', '/etc/nginx/fullchain.pem'], - ['ssl_certificate_key', '/etc/nginx/key.pem'], - ['include', self.config.parser.loc["ssl_options"]]]], + ['ssl_certificate_key', '/etc/nginx/key.pem']]+ + util.filter_comments(self.config.parser.loc["ssl_options"]) + ], 2)) def test_get_all_certs_keys(self): @@ -394,6 +395,6 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index b64f1dee3..1190a2326 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -249,5 +249,16 @@ class NginxParserTest(util.NginxTest): ]) self.assertTrue(server['ssl']) + def test_ssl_options_should_be_parsed_ssl_directives(self): + nparser = parser.NginxParser(self.config_path, self.ssl_options) + self.assertEqual(nparser.loc["ssl_options"], + [['ssl_session_cache', 'shared:SSL:1m'], + ['ssl_session_timeout', '1440m'], + ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], + ['ssl_prefer_server_ciphers', 'on'], + ['#', ' Using list of ciphers from "Bulletproof SSL and TLS"'], + ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] + ]) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py index e59281c4c..5afc950ad 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py @@ -146,7 +146,6 @@ class NginxTlsSni01(common.TLSSNI01): block.extend([['server_name', achall.response(achall.account_key).z_domain], - ['include', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) ['access_log', os.path.join( @@ -155,6 +154,7 @@ class NginxTlsSni01(common.TLSSNI01): 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]]]]) + [['location', '/'], [['root', document_root]]]] + + self.configurator.parser.loc["ssl_options"]) return [['server'], block] From 067e51170296fae8f908f60e357d99cbe62a09bc Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 12:12:29 -0500 Subject: [PATCH 006/331] Change name of session cache in order to minimize risk of conflict --- letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf b/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf index f0081c1fc..3faab8818 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf +++ b/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf @@ -1,4 +1,4 @@ -ssl_session_cache shared:SSL:1m; +ssl_session_cache shared:le_nginx_SSL:1m; ssl_session_timeout 1440m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; From 613250e1b26f4c28354932340830eb0b09e5e3cb Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 15:03:51 -0500 Subject: [PATCH 007/331] Stupid mistake, forgot to change the test --- letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index 1190a2326..3fc6a214e 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -252,7 +252,7 @@ class NginxParserTest(util.NginxTest): def test_ssl_options_should_be_parsed_ssl_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) self.assertEqual(nparser.loc["ssl_options"], - [['ssl_session_cache', 'shared:SSL:1m'], + [['ssl_session_cache', 'shared:le_nginx_SSL:1m'], ['ssl_session_timeout', '1440m'], ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], ['ssl_prefer_server_ciphers', 'on'], From f4a130eab3529fb8860669ecce3b5d2fd8393dec Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 16:28:05 -0500 Subject: [PATCH 008/331] A final small fix, hopefully --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 935bf40dd..bd9bebe08 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -175,11 +175,10 @@ class NginxParser(object): with open(ssl_options) as _file: return nginxparser.load(_file) except IOError: - logger.debug("Could not open file: %s", item) + logger.debug("Could not open file: %s", ssl_options) except pyparsing.ParseException: - logger.debug("Could not parse file: %s", item) - else: - return [] + logger.debug("Could not parse file: %s", ssl_options) + return [] def _set_locations(self, ssl_options): """Set default location for directives. From 2b097b0fb2c65939e310d3dd271f1d65a11f8c9e Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Mon, 25 Jan 2016 11:39:46 -0500 Subject: [PATCH 009/331] Fix lint issues --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 2 +- .../letsencrypt_nginx/tests/configurator_test.py | 2 +- .../letsencrypt_nginx/tests/parser_test.py | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index bd9bebe08..1cf805eef 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -179,7 +179,7 @@ class NginxParser(object): except pyparsing.ParseException: logger.debug("Could not parse file: %s", ssl_options) return [] - + def _set_locations(self, ssl_options): """Set default location for directives. diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index f35dcdfc0..c91372651 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -395,6 +395,6 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index 3fc6a214e..d66206a57 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -257,8 +257,15 @@ class NginxParserTest(util.NginxTest): ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], ['ssl_prefer_server_ciphers', 'on'], ['#', ' Using list of ciphers from "Bulletproof SSL and TLS"'], - ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] + ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-'+ + 'AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256'+ + '-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384'+ + ' ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384'+ + ' ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-'+ + 'AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM'+ + '-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-'+ + 'AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] ]) - + if __name__ == "__main__": unittest.main() # pragma: no cover From 6ffa14c632775e361fdce96badca09c5e195f93d Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Mon, 25 Jan 2016 14:22:03 -0500 Subject: [PATCH 010/331] Bad merge managed to introduce whitespace again. Sigh. --- letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index f35dcdfc0..c91372651 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -395,6 +395,6 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - + if __name__ == "__main__": unittest.main() # pragma: no cover From 4f2a8f86d888599a7bb12ece864fb9737b6e801f Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 13 Jun 2016 11:52:36 -0700 Subject: [PATCH 011/331] Remove unnecessary check on registration returned. Right now the ACME client checks that the returned registration matches the registation posted, but there's no guarantee this will always be the case, and this only introduces unnecessary fragility. --- acme/acme/client.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 117ee6b7d..de7eef299 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -89,8 +89,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes :returns: Registration Resource. :rtype: `.RegistrationResource` - :raises .UnexpectedUpdate: - """ new_reg = messages.NewRegistration() if new_reg is None else new_reg assert isinstance(new_reg, messages.NewRegistration) @@ -101,12 +99,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes # "Instance of 'Field' has no key/contact member" bug: # pylint: disable=no-member - regr = self._regr_from_response(response) - if (regr.body.key != self.key.public_key() or - regr.body.contact != new_reg.contact): - raise errors.UnexpectedUpdate(regr) - - return regr + return self._regr_from_response(response) def _send_recv_regr(self, regr, body): response = self.net.post(regr.uri, body) From f3915705668f1da08077c2f5e42b317b14df462b Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 30 Jun 2016 19:01:02 -0700 Subject: [PATCH 012/331] seth and noah updated some confusing things --- README.rst | 11 +++-- docs/using.rst | 119 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index c71079f9a..59be650dd 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,7 @@ certbot.eff.org_ to find out), you can install it from there, and run it by typing ``certbot`` (or ``letsencrypt``). Because not all operating systems have packages yet, we provide a temporary solution via the ``certbot-auto`` wrapper script, which obtains some dependencies from -your OS and puts others in a python virtual environment:: +your OS and puts others in a Python virtual environment:: user@webserver:~$ wget https://dl.eff.org/certbot-auto user@webserver:~$ chmod a+x ./certbot-auto @@ -60,9 +60,12 @@ And for full command line help, you can type:: ``certbot-auto`` updates to the latest client release automatically. And since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly -the same command line flags and arguments. More details about this script and -other installation methods can be found `in the User Guide -`_. +the same command line flags and arguments. Throughout our and others' documentation +you should substitute the name of the command that certbot.eff.org_ told you +to use on your system. (``certbot-auto`` should always be run from the directory +where it has been downloaded and invoked via ``./certbot-auto``). +More details about this script and other installation methods can be found +`in the User Guide `_. How to run the client --------------------- diff --git a/docs/using.rst b/docs/using.rst index fb96bb853..bb6741cc4 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -9,34 +9,46 @@ Getting Certbot =============== To get specific instructions for installing Certbot on your OS, we recommend -visiting certbot.eff.org_. If you're offline, you can find some general -instructions `in the README / Introduction `__ +visiting certbot.eff.org_. For general information on how to install Certbot, +and the difference between the ``certbot``, ``certbot-auto``, ``letsencrypt``, +and ``letsencrypt-auto`` commands, please refer to the +`README / Introduction `__ __ installation_ .. _certbot.eff.org: https://certbot.eff.org .. _certbot-auto: -The name of the certbot command -------------------------------- - -Many platforms now have native packages that give you a ``certbot`` or (for -older packages) ``letsencrypt`` command you can run. On others, the -``certbot-auto`` / ``letsencrypt-auto`` installer and wrapper script is a -stand-in. Throughout the documentation, whenever you see references to -``certbot`` script/binary, you should substitute in the name of the command -that certbot.eff.org_ told you to use on your system (``certbot``, -``letsencrypt``, or ``certbot-auto``). +Commands +======== +The Certbot client uses a number of different "commands" (also referred +to, equivalently, as "subcommands") to request specific actions such as +obtaining, renewing, or revoking certificates. Some of the most important +and most commonly-used commands will be discussed throughout this +document; an exhaustive list also appears near the end of the document. Plugins ======= The Certbot client supports a number of different "plugins" that can be -used to obtain and/or install certificates. Plugins that can obtain a cert -are called "authenticators" and can be used with the "certonly" command. -Plugins that can install a cert are called "installers". Plugins that do both -can be used with the "certbot run" command, which is the default. +used to obtain and/or install certificates. + +Plugins that can obtain a cert are called "authenticators" and can be used with +the "certonly" command. This will carry out the steps needed to validate that you +control the domain(s) you are requesting a cert for, obtain a cert for the specified +domain(s), and place it in the ``/etc/letsencrypt`` directory on your +machine - without editing any of your server's configuration files to serve the +obtained certificate. If you specify multiple domains to authenticate, they will +all be listed in a single certificate. To obtain multiple seperate certificates +you will need to run Certbot multiple times. + +Plugins that can install a cert are called "installers" and can be used with the +"install" command. These plugins can modify your webserver's configuration to +serve your website over HTTPS using certificates obtained by certbot. + +Plugins that do both can be used with the "certbot run" command, which is the default +when no command is specified. =========== ==== ==== =============================================================== Plugin Auth Inst Notes @@ -54,7 +66,8 @@ manual_ Y N Helps you obtain a cert by giving you instructions to perf nginx_ Y Y Very experimental and not included in certbot-auto_. =========== ==== ==== =============================================================== -There are many third-party-plugins_ available. +There are many third-party-plugins_ available. Below we describe in more detail +the circumstances in which each plugin can be used, and how to use it. Apache ------ @@ -183,7 +196,54 @@ postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plu If you're interested, you can also :ref:`write your own plugin `. +Re-running Certbot +================== +Running Certbot with the ``certonly`` or ``run`` commands always requests +the creation of a single new certificate, even if you already have an +existing certificate with some of the same domain names. The ``--force-renewal``, +``--duplicate``, and ``--expand`` options control Certbot's behavior in this case. +If you don't specify a requested behavior, Certbot may ask you what you intended. + +``--force-renewal`` tells Certbot to request a new certificate +with the same domains as an existing certificate. (Each and every domain +must be explicitly specified via ``-d``.) If successful, this certificate +will be saved alongside the earlier one and symbolic links (the "``live``" +reference) will be updated to point to the new certificate. This is a +valid method of explicitly requesting the renewal of a specific individual +certificate. + +``--duplicate`` tells Certbot to create a separate, unrelated certificate +with the same domains as an existing certificate. This certificate will +be saved completely separately from the prior one. Most users probably +do not want this behavior. + +``--expand`` tells Certbot to update an existing certificate with a new +certificate that contains all of the old domains and one or more additional +new domains. + +Whenever you obtain a new certificate in any of these ways, the new +certificate exists alongside any previously-obtained certificates, whether +or not the previous certificates have expired. The generation of a new +certificate counts against several rate limits that are intended to prevent +abuse of the ACME protocol, as described +`here `__. + +Certbot also provides a ``renew`` command. This command examines *all* existing +certificates to determine whether or not each is near expiry. For any existing +certificate that is near expiry, ``certbot renew`` will attempt to obtain a +new certificate for the same domains. Unlike ``certonly``, ``renew`` acts on +multiple certificates and always takes into account whether each one is near +expiry. Because of this, ``renew`` is suitable (and designed) for automated use, +to allow your system to automatically renew each certificate when appropriate. +Since ``renew`` will only renew certificates that are near expiry it can be +run as frequently as you want - since it will usually take no action. + +Typically, ``certbot renew`` runs a reduced risk of rate-limit problems +because it renews certificates only when necessary, and because some of +the Let's Encrypt CA's rate limit policies treat the issuance of a new +certificate under these circumstances more generously. More details about +the use of ``certbot renew`` are provided below. Renewal ======= @@ -204,10 +264,11 @@ at the time the certificate was originally issued will be used for the renewal attempt, unless you specify other plugins or options. You can also specify hooks to be run before or after a certificate is -renewed. For example, if you want to use the standalone_ plugin to renew -your certificates, you may want to use a command like +renewed. For example, if you have only a single cert and you obtained it using +the standalone_ plugin, it will be used by default when renewing. In that case +you may want to use a command like this to renew your certificate. -``certbot renew --standalone --pre-hook "service nginx stop" --post-hook "service nginx start"`` +``certbot renew --pre-hook "service nginx stop" --post-hook "service nginx start"`` This will stop Nginx so standalone can bind to the necessary ports and then restart Nginx after the plugin is finished. The hooks will only be @@ -223,12 +284,13 @@ can run on a regular basis, like every week or every day). In that case, you are likely to want to use the ``-q`` or ``--quiet`` quiet flag to silence all output except errors. -The ``--force-renew`` flag may be helpful for automating renewal; -it causes the expiration time of the certificate(s) to be ignored when -considering renewal, and attempts to renew each and every installed -certificate regardless of its age. (This form is not appropriate to run -daily because each certificate will be renewed every day, which will -quickly run into the certificate authority rate limit.) +If you are manually renewing all of your certificates, the +``--force-renewal`` flag may be helpful; it causes the expiration time of +the certificate(s) to be ignored when considering renewal, and attempts to +renew each and every installed certificate regardless of its age. (This +form is not appropriate to run daily because each certificate will be +renewed every day, which will quickly run into the certificate authority +rate limit.) Note that options provided to ``certbot renew`` will apply to *every* certificate for which renewal is attempted; for example, @@ -238,7 +300,6 @@ RSA public key. If a certificate is successfully renewed using specified options, those options will be saved and used for future renewals of that certificate. - An alternative form that provides for more fine-grained control over the renewal process (while renewing specified certificates one at a time), is ``certbot certonly`` with the complete set of subject domains of @@ -253,8 +314,8 @@ this case in order to renew and replace the old certificate rather than obtaining a new one; don't forget any `www.` domains! Specifying a subset of the domains creates a new, separate certificate containing only those domains, rather than replacing the original certificate.) -The ``certonly`` form attempts to renew one individual certificate. - +When run with a set of domains corresponding to an existing certificate, +the ``certonly`` command attempts to renew that one individual certificate. Please note that the CA will send notification emails to the address you provide if you do not renew certificates that are about to expire. From 4a9846b91184fcd95947c12e95a6d1d8ceb6a928 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Jul 2016 20:02:40 -0700 Subject: [PATCH 013/331] Begin work on multi-topic help listings --- certbot/cli.py | 84 +++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 35b3b74ae..fe3a26c87 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -497,16 +497,23 @@ class HelpfulArgumentParser(object): pass return True - def add(self, topic, *args, **kwargs): + def add(self, topics, *args, **kwargs): """Add a new command line argument. - :param str: help topic this should be listed under, can be None for - "always documented" + :param topics: str or [str] help topic(s) this should be listed under, + or None for "always documented" :param list *args: the names of this argument flag :param dict **kwargs: various argparse settings for this argument """ + if isinstance(topics, list): + # if this flag can be listed in multiple sections, try to pick the one + # that the user has asked for help about + topic = self.help_arg if self.help_arg in topics else topics[0] + else: + topic = topics # there's only one + if self.detect_defaults: kwargs = self.modify_kwargs_for_default_detection(**kwargs) @@ -607,6 +614,34 @@ class HelpfulArgumentParser(object): else: return dict([(t, t == chosen_topic) for t in self.help_topics]) +def _add_all_groups(helpful): + helpful.add_group("automation", description="Arguments for automating execution & other tweaks") + helpful.add_group("security", description="Security parameters & server settings") + helpful.add_group( + "testing", description="The following flags are meant for " + "testing purposes only! Do NOT change them, unless you " + "really know what you're doing!") + # VERBS + helpful.add_group( + "renew", description="The 'renew' subcommand will attempt to renew all" + " certificates (or more precisely, certificate lineages) you have" + " previously obtained if they are close to expiry, and print a" + " summary of the results. By default, 'renew' will reuse the options" + " used to create obtain or most recently successfully renew each" + " certificate lineage. You can try it with `--dry-run` first. For" + " more fine-grained control, you can renew individual lineages with" + " the `certonly` subcommand. Hooks are available to run commands " + " before and after renewal; see" + " https://certbot.eff.org/docs/using.html#renewal for more information on these.") + helpful.add_group("certonly", description="Options for modifying how a cert is obtained") + helpful.add_group("install", description="Options for modifying how a cert is deployed") + helpful.add_group("revoke", description="Options for revocation of certs") + helpful.add_group("rollback", description="Options for reverting config changes") + helpful.add_group("plugins", description='Options for the "plugins" subcommand') + helpful.add_group("config_changes", + description="Options for showing a history of config changes") + helpful.add_group("paths", description="Arguments changing execution paths & servers") + def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements """Returns parsed command line arguments. @@ -622,6 +657,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis # pylint: disable=too-many-statements helpful = HelpfulArgumentParser(args, plugins, detect_defaults) + _add_all_groups(helpful) # --help is automatically provided by argparse helpful.add( @@ -678,11 +714,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains " "as a parameter.") - helpful.add_group( - "automation", - description="Arguments for automating execution & other tweaks") helpful.add( - "automation", "--keep-until-expiring", "--keep", "--reinstall", + ["automation", "renew"], "--keep-until-expiring", "--keep", "--reinstall", dest="reinstall", action="store_true", help="If the requested cert matches an existing cert, always keep the " "existing one until it is due for renewal (for the " @@ -721,23 +754,19 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "(both can be renewed in parallel)") helpful.add( "automation", "--os-packages-only", action="store_true", - help="(letsencrypt-auto only) install OS package dependencies and then stop") + help="(certbot-auto only) install OS package dependencies and then stop") helpful.add( "automation", "--no-self-upgrade", action="store_true", - help="(letsencrypt-auto only) prevent the letsencrypt-auto script from" + help="(certbot-auto only) prevent the certbot-auto script from" " upgrading itself to newer released versions") helpful.add( "automation", "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") - helpful.add_group( - "testing", description="The following flags are meant for " - "testing purposes only! Do NOT change them, unless you " - "really know what you're doing!") helpful.add( "testing", "--debug", action="store_true", - help="Show tracebacks in case of errors, and allow letsencrypt-auto " + help="Show tracebacks in case of errors, and allow certbot-auto " "execution on experimental platforms") helpful.add( "testing", "--no-verify-ssl", action="store_true", @@ -754,8 +783,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "testing", "--break-my-certs", action="store_true", help="Be willing to replace or renew valid certs with invalid " "(testing/staging) certs") - helpful.add_group( - "security", description="Security parameters & server settings") helpful.add( "security", "--rsa-key-size", type=int, metavar="N", default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) @@ -805,18 +832,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") - helpful.add_group( - "renew", description="The 'renew' subcommand will attempt to renew all" - " certificates (or more precisely, certificate lineages) you have" - " previously obtained if they are close to expiry, and print a" - " summary of the results. By default, 'renew' will reuse the options" - " used to create obtain or most recently successfully renew each" - " certificate lineage. You can try it with `--dry-run` first. For" - " more fine-grained control, you can renew individual lineages with" - " the `certonly` subcommand. Hooks are available to run commands " - " before and after renewal; see" - " https://certbot.eff.org/docs/using.html#renewal for more information on these.") - helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates. Intended" @@ -859,13 +874,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis def _create_subparsers(helpful): - helpful.add_group("certonly", description="Options for modifying how a cert is obtained") - helpful.add_group("install", description="Options for modifying how a cert is deployed") - helpful.add_group("revoke", description="Options for revocation of certs") - helpful.add_group("rollback", description="Options for reverting config changes") - helpful.add_group("plugins", description="Plugin options") - helpful.add_group("config_changes", - description="Options for showing a history of config changes") helpful.add("config_changes", "--num", type=int, help="How many past revisions you want to be displayed") helpful.add( @@ -901,8 +909,6 @@ def _paths_parser(helpful): verb = helpful.verb if verb == "help": verb = helpful.help_arg - helpful.add_group( - "paths", description="Arguments changing execution paths & servers") cph = "Path to where cert is saved (with auth --csr), installed from or revoked." section = "paths" @@ -948,12 +954,14 @@ def _paths_parser(helpful): def _plugins_parsing(helpful, plugins): + # It's nuts, but there are two "plugins" topics. Somehow this works helpful.add_group( - "plugins", description="Certbot client supports an " + "plugins", description="Plugin Selection: Certbot client supports an " "extensible plugins architecture. See '%(prog)s plugins' for a " "list of all installed plugins and their names. You can force " "a particular plugin by setting options provided below. Running " "--help will list flags specific to that plugin.") + helpful.add( "plugins", "-a", "--authenticator", help="Authenticator plugin name.") helpful.add( From d96278505e6b01f115c597aaf035452924714425 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Jul 2016 20:07:33 -0700 Subject: [PATCH 014/331] Fix test --- certbot/tests/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 896550837..388c38152 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -113,7 +113,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods out = self._help_output(['--help', 'plugins']) self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--prepare" in out) - self.assertTrue("Plugin options" in out) + self.assertTrue('"plugins" subcommand' in out) out = self._help_output(['--help', 'install']) self.assertTrue("--cert-path" in out) From 204c3f0dfbd242751efaf7a36ced35f7e5d16ca5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 7 Jul 2016 15:11:23 -0700 Subject: [PATCH 015/331] Start using multi-topic CLI flag documentation --- certbot/cli.py | 53 +++++++++++++++++++++------------------ certbot/tests/cli_test.py | 8 ++++++ 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index fe3a26c87..5b7f6ca8a 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -88,8 +88,8 @@ More detailed help: the available topics are: all, automation, paths, security, testing, or any of the subcommands or - plugins (certonly, install, register, nginx, apache, standalone, webroot, - etc.) + plugins (certonly, renew, install, register, nginx, apache, standalone, + webroot, etc.) """ @@ -390,8 +390,7 @@ class HelpfulArgumentParser(object): if getattr(parsed_args, arg): raise errors.Error( ("Conflicting values for displayer." - " {0} conflicts with dialog_mode").format(arg) - ) + " {0} conflicts with dialog_mode").format(arg)) if parsed_args.validate_hooks: hooks.validate_hooks(parsed_args) @@ -676,9 +675,18 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "which ones are required if it finds one missing") helpful.add( None, "--dialog", dest="dialog_mode", action="store_true", - help="Run using dialog") + help="Run using interactive dialog menus") helpful.add( - None, "--dry-run", action="store_true", dest="dry_run", + [None, "run", "certonly"], + "-d", "--domains", "--domain", dest="domains", + metavar="DOMAIN", action=_DomainsAction, default=[], + help="Domain names to apply. For multiple domains you can use " + "multiple -d flags or enter a comma separated list of domains " + "as a parameter.") + + helpful.add( + [None, "testing", "renew", "certonly"], + "--dry-run", action="store_true", dest="dry_run", help="Perform a test run of the client, obtaining test (invalid) certs" " but not saving them to disk. This can currently only be used" " with the 'certonly' and 'renew' subcommands. \nNote: Although --dry-run" @@ -705,17 +713,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "with an existing registration, such as the e-mail address, " "should be updated, rather than registering a new account.") helpful.add(None, "-m", "--email", help=config_help("email")) - # positional arg shadows --domains, instead of appending, and - # --domains is useful, because it can be stored in config - #for subparser in parser_run, parser_auth, parser_install: - # subparser.add_argument("domains", nargs="*", metavar="domain") - helpful.add(None, "-d", "--domains", "--domain", dest="domains", - metavar="DOMAIN", action=_DomainsAction, default=[], - help="Domain names to apply. For multiple domains you can use " - "multiple -d flags or enter a comma separated list of domains " - "as a parameter.") helpful.add( - ["automation", "renew"], "--keep-until-expiring", "--keep", "--reinstall", + ["automation", "renew", "certonly", "run"], + "--keep-until-expiring", "--keep", "--reinstall", dest="reinstall", action="store_true", help="If the requested cert matches an existing cert, always keep the " "existing one until it is due for renewal (for the " @@ -729,14 +729,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis version="%(prog)s {0}".format(certbot.__version__), help="show program's version number and exit") helpful.add( - "automation", "--force-renewal", "--renew-by-default", + ["automation", "renew"], + "--force-renewal", "--renew-by-default", action="store_true", dest="renew_by_default", help="If a certificate " "already exists for the requested domains, renew it now, " "regardless of whether it is near expiry. (Often " "--keep-until-expiring is more appropriate). Also implies " "--expand.") helpful.add( - "automation", "--allow-subset-of-names", action="store_true", + ["automation", "renew"], + "--allow-subset-of-names", action="store_true", help="When performing domain validation, do not consider it a failure " "if authorizations can not be obtained for a strict subset of " "the requested domains. This may be useful for allowing renewals for " @@ -760,7 +762,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="(certbot-auto only) prevent the certbot-auto script from" " upgrading itself to newer released versions") helpful.add( - "automation", "-q", "--quiet", dest="quiet", action="store_true", + ["automation", "renew"], + "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") @@ -970,15 +973,17 @@ def _plugins_parsing(helpful, plugins): "plugins", "--configurator", help="Name of the plugin that is " "both an authenticator and an installer. Should not be used " "together with --authenticator or --installer.") - helpful.add("plugins", "--apache", action="store_true", + helpful.add(["plugins", "certonly", "run", "install"], + "--apache", action="store_true", help="Obtain and install certs using Apache") - helpful.add("plugins", "--nginx", action="store_true", + helpful.add(["plugins", "certonly", "run", "install"], + "--nginx", action="store_true", help="Obtain and install certs using Nginx") - helpful.add("plugins", "--standalone", action="store_true", + helpful.add(["plugins", "certonly"], "--standalone", action="store_true", help='Obtain certs using a "standalone" webserver.') - helpful.add("plugins", "--manual", action="store_true", + helpful.add(["plugins", "certonly"], "--manual", action="store_true", help='Provide laborious manual instructions for obtaining a cert') - helpful.add("plugins", "--webroot", action="store_true", + helpful.add(["plugins", "certonly"], "--webroot", action="store_true", help='Obtain certs by placing files in a webroot directory.') # things should not be reorder past/pre this comment: diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 388c38152..63c682b20 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -115,6 +115,14 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue("--prepare" in out) self.assertTrue('"plugins" subcommand' in out) + # test multiple topics + out = self._help_output(['-h', 'renew']) + self.assertTrue("--keep" in out) + out = self._help_output(['-h', 'automation']) + self.assertTrue("--keep" in out) + out = self._help_output(['-h', 'revoke']) + self.assertTrue("--keep" not in out) + out = self._help_output(['--help', 'install']) self.assertTrue("--cert-path" in out) self.assertTrue("--key-path" in out) From 44113a5d068ef224295994fcd60f9024b0a6854d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 7 Jul 2016 17:25:09 -0700 Subject: [PATCH 016/331] Automatically enable EPEL (after prompting users) --- letsencrypt-auto-source/letsencrypt-auto | 24 +++++++++++++++---- .../pieces/bootstrappers/rpm_common.sh | 24 +++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 22c914ed8..4abe7be38 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -281,6 +281,26 @@ BootstrapRpmCommon() { exit 1 fi + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool list *virtualenv > /dev/null 2>&1; then + echo "To use Certbot, packages from the EPEL repository need to be installed." + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s + fi + if ! $SUDO $tool install $yes_flag epel-release; then + echo "Could not enable EPEL. Aborting bootstrap!" + exit 1 + fi + fi + pkgs=" gcc dialog @@ -318,10 +338,6 @@ BootstrapRpmCommon() { " fi - if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" - fi - if ! $SUDO $tool install $yes_flag $pkgs; then echo "Could not install OS dependencies. Aborting bootstrap!" exit 1 diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 0f98b4bbc..e9865aed3 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -17,6 +17,26 @@ BootstrapRpmCommon() { exit 1 fi + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool list *virtualenv > /dev/null 2>&1; then + echo "To use Certbot, packages from the EPEL repository need to be installed." + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s + fi + if ! $SUDO $tool install $yes_flag epel-release; then + echo "Could not enable EPEL. Aborting bootstrap!" + exit 1 + fi + fi + pkgs=" gcc dialog @@ -54,10 +74,6 @@ BootstrapRpmCommon() { " fi - if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" - fi - if ! $SUDO $tool install $yes_flag $pkgs; then echo "Could not install OS dependencies. Aborting bootstrap!" exit 1 From ecd1ca4645fe594df805d1e4b6afa2c6b4441245 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 8 Jul 2016 10:05:56 -0700 Subject: [PATCH 017/331] grammatical improvement --- certbot/cli.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 5b7f6ca8a..89dacac0d 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -828,13 +828,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "security", "--no-staple-ocsp", action="store_false", help="Do not automatically enable OCSP Stapling.", dest="staple", default=None) - - helpful.add( "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") - helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates. Intended" From c3244df951fefdbc5158f59383f6695978dee1a6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 8 Jul 2016 10:12:57 -0700 Subject: [PATCH 018/331] more doc improvements --- certbot-apache/certbot_apache/configurator.py | 2 +- certbot/cli.py | 9 ++++----- certbot/interfaces.py | 2 +- certbot/plugins/standalone.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index d1c2b7165..50fd10895 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -83,7 +83,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ - description = "Apache Web Server - Alpha" + description = "Apache Web Server plugin - Beta" @classmethod def add_parser_arguments(cls, add): diff --git a/certbot/cli.py b/certbot/cli.py index 89dacac0d..97dfc6d7e 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -766,7 +766,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") - + # overwrites server, handled in HelpfulArgumentParser.parse_args() + helpful.add("testing", "--test-cert", "--staging", action='store_true', dest='staging', + help='Use the staging server to obtain test (invalid) certs; equivalent' + ' to --server ' + constants.STAGING_URI) helpful.add( "testing", "--debug", action="store_true", help="Show tracebacks in case of errors, and allow certbot-auto " @@ -947,10 +950,6 @@ def _paths_parser(helpful): help="Logs directory.") add("paths", "--server", default=flag_default("server"), help=config_help("server")) - # overwrites server, handled in HelpfulArgumentParser.parse_args() - add("testing", "--test-cert", "--staging", action='store_true', dest='staging', - help='Use the staging server to obtain test (invalid) certs; equivalent' - ' to --server ' + constants.STAGING_URI) def _plugins_parsing(helpful, plugins): diff --git a/certbot/interfaces.py b/certbot/interfaces.py index e4e62e0a2..d4b391378 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -227,7 +227,7 @@ class IConfig(zope.interface.Interface): "Location of renewal configuration file.") no_verify_ssl = zope.interface.Attribute( - "Disable SSL certificate verification.") + "Disable verification of the ACME server's certificate.") tls_sni_01_port = zope.interface.Attribute( "Port number to perform tls-sni-01 challenge. " "Boulder in testing mode defaults to 5001.") diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 8e1cb72a4..97aca351a 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -154,7 +154,7 @@ class Authenticator(common.Plugin): rely on any existing server program. """ - description = "Automatically use a temporary webserver" + description = "Spin up a temporary webserver" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) From 894af917778761c2cd3e9143402f71dc1070fe0f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 8 Jul 2016 14:09:26 -0700 Subject: [PATCH 019/331] Fix test --- certbot/plugins/disco_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot/plugins/disco_test.py b/certbot/plugins/disco_test.py index cef6ede8f..75544ee9b 100644 --- a/certbot/plugins/disco_test.py +++ b/certbot/plugins/disco_test.py @@ -55,9 +55,7 @@ class PluginEntryPointTest(unittest.TestCase): name, PluginEntryPoint.entry_point_to_plugin_name(entry_point)) def test_description(self): - self.assertEqual( - "Automatically use a temporary webserver", - self.plugin_ep.description) + self.assertTrue("temporary webserver" in self.plugin_ep.description) def test_description_with_name(self): self.plugin_ep.plugin_cls = mock.MagicMock(description="Desc") From 64012a6053dd97b1832ed241929df36d8963e8db Mon Sep 17 00:00:00 2001 From: Jacob Sachs Date: Wed, 15 Jun 2016 13:23:52 -0400 Subject: [PATCH 020/331] set dialog widgets to use autowidgetsize --- certbot/display/util.py | 21 +++++++-------------- certbot/tests/display/util_test.py | 7 ++----- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 39486b2bd..258afd948 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -82,7 +82,7 @@ class NcursesDisplay(object): def __init__(self, width=WIDTH, height=HEIGHT): super(NcursesDisplay, self).__init__() - self.dialog = dialog.Dialog() + self.dialog = dialog.Dialog(autowidgetsize=True) assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?" self.width = width self.height = height @@ -101,7 +101,7 @@ class NcursesDisplay(object): :param bool pause: Not applicable to NcursesDisplay """ - self.dialog.msgbox(message, height, width=self.width) + self.dialog.msgbox(message) def menu(self, message, choices, ok_label="OK", cancel_label="Cancel", help_label="", **unused_kwargs): @@ -170,11 +170,7 @@ class NcursesDisplay(object): `string` - input entered by the user """ - sections = message.split("\n") - # each section takes at least one line, plus extras if it's longer than self.width - wordlines = [1 + (len(section) / self.width) for section in sections] - height = 6 + sum(wordlines) + len(sections) - return _clean(self.dialog.inputbox(message, width=self.width, height=height)) + return self.dialog.inputbox(message) def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): """Display a Yes/No dialog box. @@ -191,8 +187,7 @@ class NcursesDisplay(object): """ return self.dialog.DIALOG_OK == self.dialog.yesno( - message, self.height, self.width, - yes_label=yes_label, no_label=no_label) + message, yes_label=yes_label, no_label=no_label) def checklist(self, message, tags, default_status=True, **unused_kwargs): """Displays a checklist. @@ -210,8 +205,7 @@ class NcursesDisplay(object): """ choices = [(tag, "", default_status) for tag in tags] - return _clean(self.dialog.checklist( - message, width=self.width, height=self.height, choices=choices)) + return self.dialog.checklist(message, choices=choices) def directory_select(self, message, **unused_kwargs): """Display a directory selection screen. @@ -224,9 +218,8 @@ class NcursesDisplay(object): """ root_directory = os.path.abspath(os.sep) - return _clean(self.dialog.dselect( - filepath=root_directory, width=self.width, - height=self.height, help_button=True, title=message)) + return self.dialog.dselect( + filepath=root_directory, help_button=True, title=message) @zope.interface.implementer(interfaces.IDisplay) diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index a6ced90ab..00ee6be36 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -107,8 +107,7 @@ class NcursesDisplayTest(unittest.TestCase): self.assertTrue(self.displayer.yesno("message")) mock_yesno.assert_called_with( - "message", display_util.HEIGHT, display_util.WIDTH, - yes_label="Yes", no_label="No") + "message", yes_label="Yes", no_label="No") @mock.patch("certbot.display.util." "dialog.Dialog.checklist") @@ -121,9 +120,7 @@ class NcursesDisplayTest(unittest.TestCase): (TAGS[1], "", True), (TAGS[2], "", True), ] - mock_checklist.assert_called_with( - "message", width=display_util.WIDTH, height=display_util.HEIGHT, - choices=choices) + mock_checklist.assert_called_with("message", choices=choices) @mock.patch("certbot.display.util.dialog.Dialog.dselect") def test_directory_select(self, mock_dselect): From 9c915b0ae4e42b3c4158b32efe35ae3bdfb1d476 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 14 Jul 2016 18:15:01 -0700 Subject: [PATCH 021/331] Fix tests --- certbot-nginx/certbot_nginx/parser.py | 2 +- certbot-nginx/certbot_nginx/tests/parser_test.py | 2 +- certbot-nginx/certbot_nginx/tests/util.py | 13 ++++++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index f208d0833..5b6860690 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -177,7 +177,7 @@ class NginxParser(object): if ssl_options is not None: try: with open(ssl_options) as _file: - return nginxparser.load(_file) + return nginxparser.load(_file).spaced except IOError: logger.debug("Could not open file: %s", ssl_options) except pyparsing.ParseException: diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index ddd375d96..d0bc32297 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -251,7 +251,7 @@ class NginxParserTest(util.NginxTest): def test_ssl_options_should_be_parsed_ssl_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) - self.assertEqual(nparser.loc["ssl_options"], + self.assertEqual(nginxparser.UnspacedList(nparser.loc["ssl_options"]), [['ssl_session_cache', 'shared:le_nginx_SSL:1m'], ['ssl_session_timeout', '1440m'], ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 866e5a9c7..96fdac527 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -17,6 +17,7 @@ from certbot.plugins import common from certbot_nginx import constants from certbot_nginx import configurator +from certbot_nginx import nginxparser class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods @@ -84,14 +85,20 @@ def filter_comments(tree): def traverse(tree): """Generator dropping comment nodes""" for entry in tree: - key, values = entry + # key, values = entry + spaceless = [e for e in entry if not nginxparser.spacey(e)] + if spaceless: + key = spaceless[0] + values = spaceless[1] if len(spaceless) > 1 else None + else: + key = values = "" if isinstance(key, list): new = copy.deepcopy(entry) new[1] = filter_comments(values) yield new else: - if key != '#': - yield entry + if key != '#' and spaceless: + yield spaceless return list(traverse(tree)) From dbb2398270e611a1f7211cf95f7b8e60ffa3aec0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 15 Jul 2016 09:25:12 -0700 Subject: [PATCH 022/331] Add _comment_spaced_block --- certbot-nginx/certbot_nginx/options-ssl-nginx.conf | 1 - certbot-nginx/certbot_nginx/parser.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/options-ssl-nginx.conf b/certbot-nginx/certbot_nginx/options-ssl-nginx.conf index 3faab8818..89c920b3e 100644 --- a/certbot-nginx/certbot_nginx/options-ssl-nginx.conf +++ b/certbot-nginx/certbot_nginx/options-ssl-nginx.conf @@ -4,5 +4,4 @@ ssl_session_timeout 1440m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; -# Using list of ciphers from "Bulletproof SSL and TLS" ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"; diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 5b6860690..5bfd9182b 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -566,3 +566,17 @@ def _add_directive(block, directive, replace): directive, block[location])) else: block.append(directive) + +def _comment_spaced_block(block): + """Adds a "managed by Certbot" comment to every directive.""" + comment = " # managed by Certbot" + indent = 80 - len(comment) + for i, entry in enumerate(block): + if isinstance(entry, list): + line = "".join(entry) + line = "".join(c for c in line if c != "\n") + linelength = len(line) + extra = indent - linelength + if extra < 0: + extra = 0 + block[i][-1] += extra * " " + comment From 5d7ef49fac778aa266a8f9d2d5ab9fc3a1030150 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 18 Jul 2016 15:25:09 -0700 Subject: [PATCH 023/331] _add_directive cleanup --- certbot-nginx/certbot_nginx/parser.py | 44 +++++++++++---------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 5bfd9182b..cd256d0c4 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -518,7 +518,9 @@ def _add_directives(block, directives, replace): for directive in directives: _add_directive(block, directive, replace) -repeatable_directives = set(['server_name', 'listen', 'include']) + +REPEATABLE_DIRECTIVES = set(['server_name', 'listen', 'include']) + def _add_directive(block, directive, replace): """Adds or replaces a single directive in a config block. @@ -527,27 +529,20 @@ def _add_directive(block, directive, replace): """ directive = nginxparser.UnspacedList(directive) - if len(directive) == 0: - # whitespace + if len(directive) == 0 or directive[0] == '#': + # whitespace or comment block.append(directive) return - if directive[0] == '#': - 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. - for index, line in enumerate(block): - if len(line) > 0 and line[0] == directive[0]: - location = index - break + # the name of the directive we want to add. If no line exists, use None. + location = next((index for index, line in enumerate(block) + if line and line[0] == directive[0]), None) if replace: - if location == -1: + if location is None: raise errors.MisconfigurationError( - 'expected directive for %s in the Nginx ' - 'config but did not find it.' % directive[0]) + 'expected directive for {0} in the Nginx ' + 'config but did not find it.'.format(directive[0])) block[location] = directive else: # Append directive. Fail if the name is not a repeatable directive name, @@ -555,17 +550,14 @@ def _add_directive(block, directive, replace): # in the config file. directive_name = directive[0] directive_value = directive[1] - if location != -1 and directive_name.__str__() not in repeatable_directives: - if block[location][1] == directive_value: - # There's a conflict, but the existing value matches the one we - # want to insert, so it's fine. - pass - else: - raise errors.MisconfigurationError( - 'tried to insert directive "%s" but found conflicting "%s".' % ( - directive, block[location])) - else: + if location is None or (isinstance(directive_name, str) and + directive_name in REPEATABLE_DIRECTIVES): block.append(directive) + elif block[location][1] != directive_value: + raise errors.MisconfigurationError( + 'tried to insert directive "{0}" but found ' + 'conflicting "{1}".'.format(directive, block[location])) + def _comment_spaced_block(block): """Adds a "managed by Certbot" comment to every directive.""" From aa33c0fa83fd7230af457074d4142fb29c452588 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 18 Jul 2016 15:33:28 -0700 Subject: [PATCH 024/331] does it work? --- certbot-nginx/certbot_nginx/parser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index cd256d0c4..5350e04d7 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -520,6 +520,7 @@ def _add_directives(block, directives, replace): REPEATABLE_DIRECTIVES = set(['server_name', 'listen', 'include']) +COMMENT = [" ", "#", " managed by Certbot"] def _add_directive(block, directive, replace): @@ -544,6 +545,7 @@ def _add_directive(block, directive, replace): 'expected directive for {0} in the Nginx ' 'config but did not find it.'.format(directive[0])) block[location] = directive + block.insert(location + 1, COMMENT) else: # Append directive. Fail if the name is not a repeatable directive name, # and there is already a copy of that directive with a different value @@ -553,6 +555,7 @@ def _add_directive(block, directive, replace): if location is None or (isinstance(directive_name, str) and directive_name in REPEATABLE_DIRECTIVES): block.append(directive) + block.append(COMMENT) elif block[location][1] != directive_value: raise errors.MisconfigurationError( 'tried to insert directive "{0}" but found ' From bd21325fcdd13c88b443233d1d55939d6868eadd Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 18 Jul 2016 18:12:44 -0700 Subject: [PATCH 025/331] newline logic --- certbot-nginx/certbot_nginx/parser.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 5350e04d7..57ea2db56 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -517,10 +517,26 @@ def _add_directives(block, directives, replace): """ for directive in directives: _add_directive(block, directive, replace) - + last = block[-1] + if not (isinstance(last, str) and '\n' in last): + block.append('\n') + REPEATABLE_DIRECTIVES = set(['server_name', 'listen', 'include']) -COMMENT = [" ", "#", " managed by Certbot"] +COMMENT_STR = ' managed by Certbot' +COMMENT = [' ', '#', ' managed by Certbot'] + + +def _comment_directive(block, location): + """Add a comment to the end of the line at location.""" + block.insert(location + 1, COMMENT[:]) + + if len(block) > location + 2: # there is a block after us + next_entry = block[location + 2] + if isinstance(next_entry, list): + next_entry = next_entry.spaced[0] + if "\n" not in next_entry: + block.insert(location + 2, '\n') def _add_directive(block, directive, replace): From 5dd8f70e567f4540e75b0cac0c3a4fbc0769b137 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 18 Jul 2016 18:19:14 -0700 Subject: [PATCH 026/331] better newline logic --- certbot-nginx/certbot_nginx/parser.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 57ea2db56..e8fd50452 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -529,14 +529,19 @@ COMMENT = [' ', '#', ' managed by Certbot'] def _comment_directive(block, location): """Add a comment to the end of the line at location.""" + if len(block) > location + 1: # there is a block after us + next_entry = block[location + 1] + else: + # we're at the end of the block, pretend there's a newline after us; it will actually be added later in + # add_directives + next_entry = "\n" + if isinstance(next_entry, list): + if COMMENT[-1] in next_entry[-1]: + return + next_entry = next_entry.spaced[0] block.insert(location + 1, COMMENT[:]) - - if len(block) > location + 2: # there is a block after us - next_entry = block[location + 2] - if isinstance(next_entry, list): - next_entry = next_entry.spaced[0] - if "\n" not in next_entry: - block.insert(location + 2, '\n') + if "\n" not in next_entry: + block.insert(location + 2, '\n') def _add_directive(block, directive, replace): From ed4fc9d2f73631b9fa57f111ac1ee51a7f0fd4c6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 18 Jul 2016 18:20:21 -0700 Subject: [PATCH 027/331] call _comment_directive --- certbot-nginx/certbot_nginx/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index e8fd50452..a79136836 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -566,7 +566,7 @@ def _add_directive(block, directive, replace): 'expected directive for {0} in the Nginx ' 'config but did not find it.'.format(directive[0])) block[location] = directive - block.insert(location + 1, COMMENT) + _comment_directive(block, location) else: # Append directive. Fail if the name is not a repeatable directive name, # and there is already a copy of that directive with a different value @@ -576,7 +576,7 @@ def _add_directive(block, directive, replace): if location is None or (isinstance(directive_name, str) and directive_name in REPEATABLE_DIRECTIVES): block.append(directive) - block.append(COMMENT) + _comment_directive(block, len(block)) elif block[location][1] != directive_value: raise errors.MisconfigurationError( 'tried to insert directive "{0}" but found ' From 2ce5b195e54d50881ebb506e070cb74108b38384 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 18 Jul 2016 18:23:54 -0700 Subject: [PATCH 028/331] check certbot --- certbot-nginx/certbot_nginx/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index a79136836..b7bfebb32 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -536,7 +536,7 @@ def _comment_directive(block, location): # add_directives next_entry = "\n" if isinstance(next_entry, list): - if COMMENT[-1] in next_entry[-1]: + if "Certbot" in next_entry[-1]: return next_entry = next_entry.spaced[0] block.insert(location + 1, COMMENT[:]) From e5cb04ee7da2f498ca5e08598980ca9c2e7914d3 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 21 Jul 2016 13:26:57 -0700 Subject: [PATCH 029/331] A couple of fixes --- certbot-nginx/certbot_nginx/parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index b7bfebb32..71521110e 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -520,7 +520,7 @@ def _add_directives(block, directives, replace): last = block[-1] if not (isinstance(last, str) and '\n' in last): block.append('\n') - + REPEATABLE_DIRECTIVES = set(['server_name', 'listen', 'include']) COMMENT_STR = ' managed by Certbot' @@ -531,7 +531,7 @@ def _comment_directive(block, location): """Add a comment to the end of the line at location.""" if len(block) > location + 1: # there is a block after us next_entry = block[location + 1] - else: + else: # we're at the end of the block, pretend there's a newline after us; it will actually be added later in # add_directives next_entry = "\n" @@ -576,7 +576,7 @@ def _add_directive(block, directive, replace): if location is None or (isinstance(directive_name, str) and directive_name in REPEATABLE_DIRECTIVES): block.append(directive) - _comment_directive(block, len(block)) + _comment_directive(block, len(block) - 1) elif block[location][1] != directive_value: raise errors.MisconfigurationError( 'tried to insert directive "{0}" but found ' From 85d9ab4d5c2d2afd3c36229a771f77e4536853cf Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 21 Jul 2016 13:39:13 -0700 Subject: [PATCH 030/331] UnspacedList._spaced_position: support the slice at the end fo the list - Which is needed for .insert()ing at the end, for instance. --- certbot-nginx/certbot_nginx/nginxparser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1859777d8..a4d4a452f 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -279,6 +279,9 @@ class UnspacedList(list): # Normalize indexes like list[-1] etc, and save the result if idx < 0: idx = len(self) + idx + if idx == len(self): + # not an index, but the slice at the end of the list + return len(self.spaced) if not 0 <= idx < len(self): raise IndexError("list index out of range") idx0 = idx From f98470d4a05620e09eea6992ccd6deda15ffacbf Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 26 Jul 2016 17:01:24 -0700 Subject: [PATCH 031/331] Revert "UnspacedList._spaced_position: support the slice at the end fo the list" This reverts commit 85d9ab4d5c2d2afd3c36229a771f77e4536853cf. --- certbot-nginx/certbot_nginx/nginxparser.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index a4d4a452f..1859777d8 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -279,9 +279,6 @@ class UnspacedList(list): # Normalize indexes like list[-1] etc, and save the result if idx < 0: idx = len(self) + idx - if idx == len(self): - # not an index, but the slice at the end of the list - return len(self.spaced) if not 0 <= idx < len(self): raise IndexError("list index out of range") idx0 = idx From 4eb38fe1670bea6d9e49cd7cb8fd3be46a404be8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 26 Jul 2016 17:09:01 -0700 Subject: [PATCH 032/331] Make spaced list handle an insert past the end of the list --- certbot-nginx/certbot_nginx/nginxparser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1859777d8..edb280d08 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -215,7 +215,8 @@ class UnspacedList(list): def insert(self, i, x): item, spaced_item = self._coerce(x) - self.spaced.insert(self._spaced_position(i), spaced_item) + self.spaced.insert(self._spaced_position(i) if i < len(self) else i, + spaced_item) list.insert(self, i, item) self.dirty = True From e1f560dca3acde9f10687233eda8e2d7915e68a4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 26 Jul 2016 17:23:24 -0700 Subject: [PATCH 033/331] Neaten --- certbot-nginx/certbot_nginx/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 71521110e..9dcee8bef 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -532,8 +532,8 @@ def _comment_directive(block, location): if len(block) > location + 1: # there is a block after us next_entry = block[location + 1] else: - # we're at the end of the block, pretend there's a newline after us; it will actually be added later in - # add_directives + # we're at the end of the block, pretend there's a newline after us; + # it will actually be added later in add_directives next_entry = "\n" if isinstance(next_entry, list): if "Certbot" in next_entry[-1]: From 1060ea7c3d21693bfe7cbc3aadc92dbc901febe9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 26 Jul 2016 17:36:58 -0700 Subject: [PATCH 034/331] delint --- certbot-nginx/certbot_nginx/parser.py | 4 ++-- certbot-nginx/certbot_nginx/tests/configurator_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index c0ce6f185..8500179b1 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -532,8 +532,8 @@ def _comment_directive(block, location): if len(block) > location + 1: # there is a block after us next_entry = block[location + 1] else: - # we're at the end of the block, pretend there's a newline after us; it will actually be added later in - # add_directives + # we're at the end of the block, pretend there's a newline after us; it + # will actually be added later in add_directives next_entry = "\n" if isinstance(next_entry, list): if "Certbot" in next_entry[-1]: diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index d8fc849b7..f77b9d7cd 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -234,7 +234,7 @@ class NginxConfiguratorTest(util.NginxTest): ['index', 'index.html index.htm']]], ['listen', '5001 ssl'], ['ssl_certificate', '/etc/nginx/fullchain.pem'], - ['ssl_certificate_key', '/etc/nginx/key.pem']]+ + ['ssl_certificate_key', '/etc/nginx/key.pem']] + util.filter_comments(self.config.parser.loc["ssl_options"]) ], 2)) From df42f69d8ce646367aa382f982a2124031567b29 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 26 Jul 2016 18:08:21 -0700 Subject: [PATCH 035/331] Address review comments --- certbot/cli.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 9537af5f3..a93e072e3 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -503,7 +503,9 @@ class HelpfulArgumentParser(object): """Add a new command line argument. :param topics: str or [str] help topic(s) this should be listed under, - or None for "always documented" + or None for "always documented". The first entry + determines where the flag lives in the "--help all" + output (None -> "optional arguments"). :param list *args: the names of this argument flag :param dict **kwargs: various argparse settings for this argument @@ -671,7 +673,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") helpful.add( - None, "-n", "--non-interactive", "--noninteractive", + [None, "automation"], "-n", "--non-interactive", "--noninteractive", dest="noninteractive_mode", action="store_true", help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " @@ -701,7 +703,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " if they are defined because they may be necessary to accurately simulate" " renewal. --renew-hook commands are not called.") helpful.add( - None, "--register-unsafely-without-email", action="store_true", + ["register", "automation"], "--register-unsafely-without-email", action="store_true", help="Specifying this flag enables registering an account with no " "email address. This is strongly discouraged, because in the " "event of key loss or account compromise you will irrevocably " @@ -740,7 +742,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "--keep-until-expiring is more appropriate). Also implies " "--expand.") helpful.add( - ["automation", "renew"], + ["automation", "renew", "certonly"], "--allow-subset-of-names", action="store_true", help="When performing domain validation, do not consider it a failure " "if authorizations can not be obtained for a strict subset of " From fbadf7550c81d317aa70a8238389709884049e61 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 26 Jul 2016 19:20:02 -0700 Subject: [PATCH 036/331] Also put -q in certonly --- certbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index a93e072e3..22787392d 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -767,7 +767,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="(certbot-auto only) prevent the certbot-auto script from" " upgrading itself to newer released versions") helpful.add( - ["automation", "renew"], + ["automation", "renew", "certonly"], "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") From 9ebda1879cbcdf400718ac033113dd10a53c958d Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 28 Jul 2016 15:43:57 -0700 Subject: [PATCH 037/331] Restructured installation docs. Mainly put everything together in a sensible order in using.rst and pointed to it from README.rst. --- README.rst | 35 +---- docs/using.rst | 405 +++++++++++++++++++++++++------------------------ 2 files changed, 213 insertions(+), 227 deletions(-) diff --git a/README.rst b/README.rst index c71079f9a..007b9b469 100644 --- a/README.rst +++ b/README.rst @@ -36,33 +36,9 @@ If you'd like to contribute to this project please read `Developer Guide Installation ------------ -If ``certbot`` (or ``letsencrypt``) is packaged for your Unix OS (visit -certbot.eff.org_ to find out), you can install it -from there, and run it by typing ``certbot`` (or ``letsencrypt``). Because -not all operating systems have packages yet, we provide a temporary solution -via the ``certbot-auto`` wrapper script, which obtains some dependencies from -your OS and puts others in a python virtual environment:: - - user@webserver:~$ wget https://dl.eff.org/certbot-auto - user@webserver:~$ chmod a+x ./certbot-auto - user@webserver:~$ ./certbot-auto --help - -.. hint:: The certbot-auto download is protected by HTTPS, which is pretty good, but if you'd like to - double check the integrity of the ``certbot-auto`` script, you can use these steps for verification before running it:: - - user@server:~$ wget -N https://dl.eff.org/certbot-auto.asc - user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 - user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc certbot-auto - -And for full command line help, you can type:: - - ./certbot-auto --help all - -``certbot-auto`` updates to the latest client release automatically. And -since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly -the same command line flags and arguments. More details about this script and -other installation methods can be found `in the User Guide -`_. +The easiest way to install Certbot is by visiting certbot.eff.org_, where you can +find the correct installation instructions for many web server and OS combinations. +For more information, see the `User Guide `_. How to run the client --------------------- @@ -71,6 +47,11 @@ In many cases, you can just run ``certbot-auto`` or ``certbot``, and the client will guide you through the process of obtaining and installing certs interactively. +For full command line help, you can type:: + + ./certbot-auto --help all + + You can also tell it exactly what you want it to do from the command line. For instance, if you want to obtain a cert for ``example.com``, ``www.example.com``, and ``other.example.net``, using the Apache plugin to both diff --git a/docs/using.rst b/docs/using.rst index 0945f4faf..fdd235ce0 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -5,35 +5,215 @@ User Guide .. contents:: Table of Contents :local: +.. _installation: + Getting Certbot =============== -To get specific instructions for installing Certbot on your OS, -visit certbot.eff.org_. This is the easiest way to learn how to get -Certbot up and running on your system. - -If you're offline, you can find some general -instructions `in the README / Introduction `__ - -__ installation_ .. _certbot.eff.org: https://certbot.eff.org .. _certbot-auto: https://certbot.eff.org/docs/using.html#certbot-auto -The name of the certbot command -------------------------------- +Certbot is packaged for many common operating systems and web servers. Check whether +``certbot`` (or ``letsencrypt``) is packaged for your web server's OS by visiting +certbot.eff.org_, where you will also find the correct installation instructions for +your system. -Many platforms now have native packages that give you a ``certbot`` or (for -older packages) ``letsencrypt`` command you can run. On others, the -``certbot-auto`` / ``letsencrypt-auto`` installer and wrapper script is a -stand-in. Throughout the documentation, whenever you see references to -``certbot`` script/binary, you should substitute in the name of the command -that certbot.eff.org_ told you to use on your system (``certbot``, -``letsencrypt``, or ``certbot-auto``). +.. Note:: Unless you have very specific requirements, we kindly suggest that you use the Certbot packages provided by your package manager (see certbot.eff.org_). If such packages are not available, we recommend using ``certbot-auto``, which automates the process of installing Certbot on your system. + +The ``certbot`` script on your web server might be named ``letsencrypt`` if your system uses an older package, or ``certbot-auto`` if you used an alternate installation method. Throughout the docs, whenever you see ``certbot``, swap in the correct name as needed. -Plugins -======= +Other installation methods +-------------------------- +If you are offline or your operating system doesn't provide a package, you can use +an alternate method fo install ``certbot``. + +Certbot-Auto +^^^^^^^^^^^^ +The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies +from your web server OS and putting others in a python virtual environment. You can +download and run it as follows:: + + user@webserver:~$ wget https://dl.eff.org/certbot-auto + user@webserver:~$ chmod a+x ./certbot-auto + user@webserver:~$ ./certbot-auto --help + +.. hint:: The certbot-auto download is protected by HTTPS, which is pretty good, but if you'd like to + double check the integrity of the ``certbot-auto`` script, you can use these steps for verification before running it:: + + user@server:~$ wget -N https://dl.eff.org/certbot-auto.asc + user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 + user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc certbot-auto + +The ``certbot-auto`` command updates to the latest client release automatically. +Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly +the same command line flags and arguments. For more information, see +`Certbot command-line options `_. + +Running with Docker +^^^^^^^^^^^^^^^^^^^ + +Docker_ is an amazingly simple and quick way to obtain a +certificate. However, this mode of operation is unable to install +certificates or configure your webserver, because our installer +plugins cannot reach your webserver from inside the Docker container. + +Most users should use the operating system packages (see instructions at +certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only +use Docker if you are sure you know what you are doing and have a +good reason to do so. + +You should definitely read the :ref:`where-certs` section, in order to +know how to manage the certs +manually. `Our ciphersuites page `__ +provides some information about recommended ciphersuites. If none of +these make much sense to you, you should definitely use the +certbot-auto_ method, which enables you to use installer plugins +that cover both of those hard topics. + +If you're still not convinced and have decided to use this method, +from the server that the domain you're requesting a cert for resolves +to, `install Docker`_, then issue the following command: + +.. code-block:: shell + + sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot \ + -v "/etc/letsencrypt:/etc/letsencrypt" \ + -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ + quay.io/letsencrypt/letsencrypt:latest certonly + +Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory +``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from +within Docker, you must install the certificate manually according to the procedure +recommended by the provider of your webserver. + +For more information about the layout +of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. + +.. _Docker: https://docker.com +.. _`install Docker`: https://docs.docker.com/userguide/ + + +Operating System Packages +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**FreeBSD** + + * Port: ``cd /usr/ports/security/py-certbot && make install clean`` + * Package: ``pkg install py27-certbot`` + +**OpenBSD** + + * Port: ``cd /usr/ports/security/letsencrypt/client && make install clean`` + * Package: ``pkg_add letsencrypt`` + +**Arch Linux** + +.. code-block:: shell + + sudo pacman -S certbot + +**Debian** + +If you run Debian Stretch or Debian Sid, you can install certbot packages. + +.. code-block:: shell + + sudo apt-get update + sudo apt-get install certbot python-certbot-apache + +If you don't want to use the Apache plugin, you can omit the +``python-certbot-apache`` package. + +Packages exist for Debian Jessie via backports. First you'll have to follow the +instructions at http://backports.debian.org/Instructions/ to enable the Jessie backports +repo, if you have not already done so. Then run: + +.. code-block:: shell + + sudo apt-get install letsencrypt python-letsencrypt-apache -t jessie-backports + +**Fedora** + +.. code-block:: shell + + sudo dnf install letsencrypt + +**Gentoo** + +The official Certbot client is available in Gentoo Portage. If you +want to use the Apache plugin, it has to be installed separately: + +.. code-block:: shell + + emerge -av app-crypt/letsencrypt + emerge -av app-crypt/letsencrypt-apache + +Currently, only the Apache plugin is included in Portage. However, if you +Warning! +You can use Layman to add the mrueg overlay which does include a package for the +Certbot Nginx plugin, however, this plugin is known to be buggy and should only +be used with caution after creating a backup up your Nginx configuration. +We strongly recommend you use the app-crypt/letsencrypt package instead until +the Nginx plugin is ready. + +.. code-block:: shell + + emerge -av app-portage/layman + layman -S + layman -a mrueg + emerge -av app-crypt/letsencrypt-nginx + +When using the Apache plugin, you will run into a "cannot find a cert or key +directive" error if you're sporting the default Gentoo ``httpd.conf``. +You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf`` +as follows: + +Change + +.. code-block:: shell + + + LoadModule ssl_module modules/mod_ssl.so + + +to + +.. code-block:: shell + + # + LoadModule ssl_module modules/mod_ssl.so + # + +For the time being, this is the only way for the Apache plugin to recognise +the appropriate directives when installing the certificate. +Note: this change is not required for the other plugins. + +**Other Operating Systems** + +OS packaging is an ongoing effort. If you'd like to package +Certbot for your distribution of choice please have a +look at the :doc:`packaging`. + + +Installing from source +^^^^^^^^^^^^^^^^^^^^^^ + +Installation from source is only supported for developers and the +whole process is described in the :doc:`contributing`. + +.. warning:: Please do **not** use ``python setup.py install`` or + ``python pip install .``. Please do **not** attempt the + installation commands as superuser/root and/or without virtual + environment, e.g. ``sudo python setup.py install``, ``sudo pip + install``, ``sudo ./venv/bin/...``. These modes of operation might + corrupt your operating system and are **not supported** by the + Certbot team! + + +Getting certificates +==================== The Certbot client supports a number of different "plugins" that can be used to obtain and/or install certificates. Plugins that can obtain a cert @@ -57,7 +237,7 @@ manual_ Y N Helps you obtain a cert by giving you instructions to perf nginx_ Y Y Very experimental and not included in certbot-auto_. =========== ==== ==== =============================================================== -There are many third-party-plugins_ available. +There are also many third-party-plugins_ available. Apache ------ @@ -186,10 +366,10 @@ postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plu If you're interested, you can also :ref:`write your own plugin `. +.. _renewal: - -Renewal -======= +Renewing certificates +===================== .. note:: Let's Encrypt CA issues short-lived certificates (90 days). Make sure you renew the certificates at least once in 3 @@ -268,8 +448,8 @@ commands into your individual environment. .. _command-line: -Command line options -==================== +Certbot command-line options +============================ Certbot supports a lot of command line options. Here's the full list, from ``certbot --help all``: @@ -388,179 +568,4 @@ give us as much information as possible: - your operating system, including specific version - specify which installation method you've chosen -Other methods of installation -============================= -Running with Docker -------------------- - -Docker_ is an amazingly simple and quick way to obtain a -certificate. However, this mode of operation is unable to install -certificates or configure your webserver, because our installer -plugins cannot reach your webserver from inside the Docker container. - -Most users should use the operating system packages (see instructions at -certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only -use Docker if you are sure you know what you are doing and have a -good reason to do so. - -You should definitely read the :ref:`where-certs` section, in order to -know how to manage the certs -manually. `Our ciphersuites page `__ -provides some information about recommended ciphersuites. If none of -these make much sense to you, you should definitely use the -certbot-auto_ method, which enables you to use installer plugins -that cover both of those hard topics. - -If you're still not convinced and have decided to use this method, -from the server that the domain you're requesting a cert for resolves -to, `install Docker`_, then issue the following command: - -.. code-block:: shell - - sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot \ - -v "/etc/letsencrypt:/etc/letsencrypt" \ - -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ - quay.io/letsencrypt/letsencrypt:latest certonly - -Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory -``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from -within Docker, you must install the certificate manually according to the procedure -recommended by the provider of your webserver. - -For more information about the layout -of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. - -.. _Docker: https://docker.com -.. _`install Docker`: https://docs.docker.com/userguide/ - - -Operating System Packages --------------------------- - -**FreeBSD** - - * Port: ``cd /usr/ports/security/py-certbot && make install clean`` - * Package: ``pkg install py27-certbot`` - -**OpenBSD** - - * Port: ``cd /usr/ports/security/letsencrypt/client && make install clean`` - * Package: ``pkg_add letsencrypt`` - -**Arch Linux** - -.. code-block:: shell - - sudo pacman -S certbot - -**Debian** - -If you run Debian Stretch or Debian Sid, you can install certbot packages. - -.. code-block:: shell - - sudo apt-get update - sudo apt-get install certbot python-certbot-apache - -If you don't want to use the Apache plugin, you can omit the -``python-certbot-apache`` package. - -Packages exist for Debian Jessie via backports. First you'll have to follow the -instructions at http://backports.debian.org/Instructions/ to enable the Jessie backports -repo, if you have not already done so. Then run: - -.. code-block:: shell - - sudo apt-get install letsencrypt python-letsencrypt-apache -t jessie-backports - -**Fedora** - -.. code-block:: shell - - sudo dnf install letsencrypt - -**Gentoo** - -The official Certbot client is available in Gentoo Portage. If you -want to use the Apache plugin, it has to be installed separately: - -.. code-block:: shell - - emerge -av app-crypt/letsencrypt - emerge -av app-crypt/letsencrypt-apache - -Currently, only the Apache plugin is included in Portage. However, if you -Warning! -You can use Layman to add the mrueg overlay which does include a package for the -Certbot Nginx plugin, however, this plugin is known to be buggy and should only -be used with caution after creating a backup up your Nginx configuration. -We strongly recommend you use the app-crypt/letsencrypt package instead until -the Nginx plugin is ready. - -.. code-block:: shell - - emerge -av app-portage/layman - layman -S - layman -a mrueg - emerge -av app-crypt/letsencrypt-nginx - -When using the Apache plugin, you will run into a "cannot find a cert or key -directive" error if you're sporting the default Gentoo ``httpd.conf``. -You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf`` -as follows: - -Change - -.. code-block:: shell - - - LoadModule ssl_module modules/mod_ssl.so - - -to - -.. code-block:: shell - - # - LoadModule ssl_module modules/mod_ssl.so - # - -For the time being, this is the only way for the Apache plugin to recognise -the appropriate directives when installing the certificate. -Note: this change is not required for the other plugins. - -**Other Operating Systems** - -OS packaging is an ongoing effort. If you'd like to package -Certbot for your distribution of choice please have a -look at the :doc:`packaging`. - - -From source ------------ - -Installation from source is only supported for developers and the -whole process is described in the :doc:`contributing`. - -.. warning:: Please do **not** use ``python setup.py install`` or - ``python pip install .``. Please do **not** attempt the - installation commands as superuser/root and/or without virtual - environment, e.g. ``sudo python setup.py install``, ``sudo pip - install``, ``sudo ./venv/bin/...``. These modes of operation might - corrupt your operating system and are **not supported** by the - Certbot team! - - -Comparison of different methods -------------------------------- - -Unless you have very specific requirements, we kindly suggest that you use -the Certbot packages provided by your package manager (see certbot.eff.org_). -If such packages are not available, we recommend using ``certbot-auto``, which -automates the process of installing Certbot on your system. - -Beyond the methods discussed here, other methods may be possible, such as -installing Certbot directly with pip from PyPI or downloading a ZIP -archive from GitHub may be technically possible but are not presently -recommended or supported. From 89f576babb88722f3273fe770971a4942b23ac96 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 29 Jul 2016 16:51:33 -0700 Subject: [PATCH 038/331] Primarily simple s/apache/nginx/ and the like --- .../configurators/nginx/Dockerfile | 20 +++ .../configurators/nginx/__init__.py | 1 + .../configurators/nginx/common.py | 153 ++++++++++++++++++ .../certbot_compatibility_test/test_driver.py | 5 +- 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/Dockerfile create mode 100644 certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/__init__.py create mode 100644 certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/Dockerfile b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/Dockerfile new file mode 100644 index 000000000..ea9bb857f --- /dev/null +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/Dockerfile @@ -0,0 +1,20 @@ +FROM httpd +MAINTAINER Brad Warren + +RUN mkdir /var/run/apache2 + +ENV APACHE_RUN_USER=daemon \ + APACHE_RUN_GROUP=daemon \ + APACHE_PID_FILE=/usr/local/apache2/logs/httpd.pid \ + APACHE_RUN_DIR=/var/run/apache2 \ + APACHE_LOCK_DIR=/var/lock \ + APACHE_LOG_DIR=/usr/local/apache2/logs + +COPY certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh /usr/local/bin/ +COPY certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh /usr/local/bin/ +COPY certbot-compatibility-test/certbot_compatibility_test/testdata/rsa1024_key2.pem /usr/local/apache2/conf/ +COPY certbot-compatibility-test/certbot_compatibility_test/testdata/empty_cert.pem /usr/local/apache2/conf/ + +# Note: this only exposes the port to other docker containers. You +# still have to bind to 443@host at runtime. +EXPOSE 443 diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/__init__.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/__init__.py new file mode 100644 index 000000000..d559d0645 --- /dev/null +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/__init__.py @@ -0,0 +1 @@ +"""Certbot compatibility test Apache configurators""" diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py new file mode 100644 index 000000000..29f46ca4f --- /dev/null +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -0,0 +1,153 @@ +"""Provides a common base for Apache proxies""" +import re +import os +import shutil +import subprocess + +import mock +import zope.interface + +from certbot import configuration +from certbot import errors as le_errors +from certbot_nginx import configurator +from certbot_nginx import constants +from certbot_compatibility_test import errors +from certbot_compatibility_test import interfaces +from certbot_compatibility_test import util +from certbot_compatibility_test.configurators import common as configurators_common + + +# APACHE_VERSION_REGEX = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) +# XXX APACHE_COMMANDS = ["apachectl", "a2enmod", "a2dismod"] + + +@zope.interface.implementer(interfaces.IConfiguratorProxy) +class Proxy(configurators_common.Proxy): + # pylint: disable=too-many-instance-attributes + """A common base for Nginx test configurators""" + + def __init__(self, args): + # XXX: This is still apache-specific + """Initializes the plugin with the given command line args""" + super(Proxy, self).__init__(args) + self.le_config.apache_le_vhost_ext = "-le-ssl.conf" + + self.modules = self.server_root = self.test_conf = self.version = None + self._nginx_configurator = self._all_names = self._test_names = None + patch = mock.patch( + "certbot_apache.configurator.display_ops.select_vhost") + mock_display = patch.start() + mock_display.side_effect = le_errors.PluginError( + "Unable to determine vhost") + + def __getattr__(self, name): + """Wraps the Nginx Configurator methods""" + method = getattr(self._nginx_configurator, name, None) + if callable(method): + return method + else: + raise AttributeError() + + def load_config(self): + """Loads the next configuration for the plugin to test""" + + config = super(Proxy, self).load_config() + self._all_names, self._test_names = _get_names(config) + + server_root = _get_server_root(config) + # with open(os.path.join(config, "config_file")) as f: + # config_file = os.path.join(server_root, f.readline().rstrip()) + + # XXX: Deleting all of this is kind of scary unless the test + # instances really each have a complete configuration! + shutil.rmtree("/etc/nginx") + shutil.copytree(server_root, "/etc/nginx", symlinks=True) + + self._prepare_configurator() + + try: + subprocess.check_call("nginx".split()) + except errors.Error: + raise errors.Error( + "Nginx failed to load {0} before tests started".format( + config)) + + return config + + def _prepare_configurator(self): + """Prepares the Nginx plugin for testing""" + for k in constants.CLI_DEFAULTS.keys(): + setattr(self.le_config, "nginx_" + k, constants.os_constant(k)) + + # An alias + self.le_config.nginx_handle_modules = self.le_config.nginx_handle_mods + + self._nginx_configurator = configurator.NginxConfigurator( + config=configuration.NamespaceConfig(self.le_config), + name="nginx") + self._nginx_configurator.prepare() + + def cleanup_from_tests(self): + """Performs any necessary cleanup from running plugin tests""" + super(Proxy, self).cleanup_from_tests() + mock.patch.stopall() + + def get_all_names_answer(self): + """Returns the set of domain names that the plugin should find""" + if self._all_names: + return self._all_names + else: + raise errors.Error("No configuration file loaded") + + def get_testable_domain_names(self): + """Returns the set of domain names that can be tested against""" + if self._test_names: + return self._test_names + else: + return {"example.com"} + + def deploy_cert(self, domain, cert_path, key_path, chain_path=None, + fullchain_path=None): + """Installs cert""" + cert_path, key_path, chain_path = self.copy_certs_and_keys( + cert_path, key_path, chain_path) + self._nginx_configurator.deploy_cert( + domain, cert_path, key_path, chain_path, fullchain_path) + + +def _get_server_root(config): + """Returns the server root directory in config""" + subdirs = [ + name for name in os.listdir(config) + if os.path.isdir(os.path.join(config, name))] + + if len(subdirs) != 1: + errors.Error("Malformed configuration directory {0}".format(config)) + + return os.path.join(config, subdirs[0].rstrip()) + + +def _get_names(config): + """Returns all and testable domain names in config""" + # XXX: This is still Apache-specific + all_names = set() + non_ip_names = set() + with open(os.path.join(config, "vhosts")) as f: + for line in f: + # If parsing a specific vhost + if line[0].isspace(): + words = line.split() + if words[0] == "alias": + all_names.add(words[1]) + non_ip_names.add(words[1]) + # If for port 80 and not IP vhost + elif words[1] == "80" and not util.IP_REGEX.match(words[3]): + all_names.add(words[3]) + non_ip_names.add(words[3]) + elif "NameVirtualHost" not in line: + words = line.split() + if (words[0].endswith("*") or words[0].endswith("80") and + not util.IP_REGEX.match(words[1]) and + words[1].find(".") != -1): + all_names.add(words[1]) + return all_names, non_ip_names diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 2c78eea1f..3d03f7771 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -22,7 +22,8 @@ from certbot_compatibility_test import errors from certbot_compatibility_test import util from certbot_compatibility_test import validator -from certbot_compatibility_test.configurators.apache import common +from certbot_compatibility_test.configurators.apache import common as a_common +from certbot_compatibility_test.configurators.nginx import common as n_common DESCRIPTION = """ @@ -32,7 +33,7 @@ tests that the plugin supports are performed. """ -PLUGINS = {"apache": common.Proxy} +PLUGINS = {"apache": a_common.Proxy, "nginx": n_common.Proxy} logger = logging.getLogger(__name__) From 7b67ba6797ce41aad1ec70e0ad894b90204c51f0 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 29 Jul 2016 17:14:23 -0700 Subject: [PATCH 039/331] Remove unused Apache-related variables --- .../certbot_compatibility_test/configurators/apache/common.py | 4 ---- .../certbot_compatibility_test/configurators/nginx/common.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index ed3d9d67a..0c53058de 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -17,10 +17,6 @@ from certbot_compatibility_test import util from certbot_compatibility_test.configurators import common as configurators_common -APACHE_VERSION_REGEX = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) -APACHE_COMMANDS = ["apachectl", "a2enmod", "a2dismod"] - - @zope.interface.implementer(interfaces.IConfiguratorProxy) class Proxy(configurators_common.Proxy): # pylint: disable=too-many-instance-attributes diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index 29f46ca4f..c474d078c 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -17,10 +17,6 @@ from certbot_compatibility_test import util from certbot_compatibility_test.configurators import common as configurators_common -# APACHE_VERSION_REGEX = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) -# XXX APACHE_COMMANDS = ["apachectl", "a2enmod", "a2dismod"] - - @zope.interface.implementer(interfaces.IConfiguratorProxy) class Proxy(configurators_common.Proxy): # pylint: disable=too-many-instance-attributes From 89758decbb2b1b18e8b56c49c73a6cb1c852e5ec Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 29 Jul 2016 17:28:22 -0700 Subject: [PATCH 040/331] Fix a test --- certbot-nginx/certbot_nginx/tests/configurator_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index a6cd0d588..19dcd157a 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -94,7 +94,8 @@ class NginxConfiguratorTest(util.NginxTest): ['listen', '127.0.0.1'], ['server_name', '.example.com'], ['server_name', 'example.*'], - ['listen', '5001 ssl'] + ['listen', '5001 ssl'], + ['#', ' managed by Certbot'] ]]], parsed[0]) From 353cb6e6c627e680c8e573a101a3548c193fc769 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 3 Aug 2016 17:10:20 -0700 Subject: [PATCH 041/331] New _get_names approach for nginx test --- .../configurators/nginx/common.py | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index c474d078c..779ba26a7 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -125,25 +125,12 @@ def _get_server_root(config): def _get_names(config): """Returns all and testable domain names in config""" - # XXX: This is still Apache-specific all_names = set() - non_ip_names = set() - with open(os.path.join(config, "vhosts")) as f: - for line in f: - # If parsing a specific vhost - if line[0].isspace(): - words = line.split() - if words[0] == "alias": - all_names.add(words[1]) - non_ip_names.add(words[1]) - # If for port 80 and not IP vhost - elif words[1] == "80" and not util.IP_REGEX.match(words[3]): - all_names.add(words[3]) - non_ip_names.add(words[3]) - elif "NameVirtualHost" not in line: - words = line.split() - if (words[0].endswith("*") or words[0].endswith("80") and - not util.IP_REGEX.match(words[1]) and - words[1].find(".") != -1): - all_names.add(words[1]) + for root, _dirs, files in os.walk(config): + for this_file in files: + for line in open(os.path.join(root, this_file)): + if line.strip().starts_with("server_name"): + names = line.partition("server_name")[2].rstrip(";") + [all_names.add(n) for n in names.split()] + non_ip_names = set(n for n in all_names if not util.IP_REGEX.match(n)) return all_names, non_ip_names From 1fdf41e636ca81a9871b41c48d3e2e9a75083d0d Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 27 Jul 2016 19:21:26 +0200 Subject: [PATCH 042/331] Adding modification check against the current /letsencrypt-auto --- letsencrypt-auto-source/pieces/fetch.py | 2 +- tests/modification-check.sh | 21 +++++++++++++++++++++ tox.ini | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100755 tests/modification-check.sh diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index d11f8da61..365a5a36a 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -2,7 +2,7 @@ # Print latest released version of LE to stdout: python fetch.py --latest-version - + # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm # in, and make sure its signature verifies: python fetch.py --le-auto-script v1.2.3 diff --git a/tests/modification-check.sh b/tests/modification-check.sh new file mode 100755 index 000000000..b9cc669ff --- /dev/null +++ b/tests/modification-check.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +temp_dir=`mktemp -d` + +# Script should be run from Certbot's root directory +cp letsencrypt-auto ${temp_dir}/to-be-checked +cp letsencrypt-auto-source/pieces/fetch.py ${temp_dir}/fetch.py +cd ${temp_dir} + +LATEST_VERSION=`python fetch.py --latest-version` +python fetch.py --le-auto-script v${LATEST_VERSION} + +cmp -s letsencrypt-auto to-be-checked + +if [ $? != 0 ]; then + echo "Root letsencrypt-auto has changed." + rm -rf temp_dir + exit 1 +fi + +rm -rf temp_dir diff --git a/tox.ini b/tox.ini index 27979d9df..e0fbc9502 100644 --- a/tox.ini +++ b/tox.ini @@ -84,6 +84,7 @@ commands = # At the moment, this tests under Python 2.7 only, as only that version is # readily available on the Trusty Docker image. commands = + {toxinidir}/tests/modification-check.sh docker build -t lea letsencrypt-auto-source docker run --rm -t -i lea whitelist_externals = From 2cd2228ca6a8e3e2b311d5974afa816084695bb7 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 5 Aug 2016 15:07:35 -0700 Subject: [PATCH 043/331] starts_with is actually called startswith --- .../certbot_compatibility_test/configurators/nginx/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index 779ba26a7..9fbb538e8 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -129,7 +129,7 @@ def _get_names(config): for root, _dirs, files in os.walk(config): for this_file in files: for line in open(os.path.join(root, this_file)): - if line.strip().starts_with("server_name"): + if line.strip().startswith("server_name"): names = line.partition("server_name")[2].rstrip(";") [all_names.add(n) for n in names.split()] non_ip_names = set(n for n in all_names if not util.IP_REGEX.match(n)) From ae6ca4d4ca932dd364b7c6367c96cff25987fad6 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 5 Aug 2016 15:13:04 -0700 Subject: [PATCH 044/331] Minimal fake os_constant() for nginx constants.py --- certbot-nginx/certbot_nginx/constants.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot-nginx/certbot_nginx/constants.py b/certbot-nginx/certbot_nginx/constants.py index 5dde30efc..453266878 100644 --- a/certbot-nginx/certbot_nginx/constants.py +++ b/certbot-nginx/certbot_nginx/constants.py @@ -16,3 +16,10 @@ MOD_SSL_CONF_SRC = pkg_resources.resource_filename( "certbot_nginx", "options-ssl-nginx.conf") """Path to the nginx mod_ssl config file found in the Certbot distribution.""" + +def os_constant(key): + # XXX TODO: In the future, this could return different constants + # based on what OS we are running under. To see an + # approach to how to handle different OSes, see the + # apache version of this file. + return CLI_DEFAULTS From 3a2df72bceeada0a0a4fc7b0cc7df7d926092451 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 5 Aug 2016 15:36:24 -0700 Subject: [PATCH 045/331] Add newlines to the ends of blocks more correctly --- certbot-nginx/certbot_nginx/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index ca02b6316..4577f9fa5 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -518,8 +518,8 @@ def _add_directives(block, directives, replace): for directive in directives: _add_directive(block, directive, replace) last = block[-1] - if not (isinstance(last, str) and '\n' in last): - block.append('\n') + if not '\n' in last: # could be " \n " or ["\n"] ! + block.append(nginxparser.UnspacedList('\n')) REPEATABLE_DIRECTIVES = set(['server_name', 'listen', 'include']) From cdc894601c83d75e2038c5bc9d6f97fc6fbc3eef Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 5 Aug 2016 15:36:40 -0700 Subject: [PATCH 046/331] Tolerate our own added newlines --- certbot-nginx/certbot_nginx/parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 4577f9fa5..536602e27 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -329,6 +329,8 @@ class NginxParser(object): tup = [None, None, vhost.filep] if vhost.ssl: for directive in vhost.raw: + if not directive: + continue if directive[0] == 'ssl_certificate': tup[0] = directive[1] elif directive[0] == 'ssl_certificate_key': From 460f49778f163fc137bb8eeb64135ec9dcbc9b65 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 5 Aug 2016 15:37:01 -0700 Subject: [PATCH 047/331] Fix tests for our new spacey, commented world --- certbot-nginx/certbot_nginx/tests/parser_test.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index aec6f2a23..09b1cfd0b 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -144,7 +144,10 @@ class NginxParserTest(util.NginxTest): self.assertEqual(nparser.parsed[server_conf], [['server_name', 'somename alias another.alias'], ['foo', 'bar'], - ['ssl_certificate', '/etc/ssl/cert2.pem'] + ['#', ' managed by Certbot'], + ['ssl_certificate', '/etc/ssl/cert2.pem'], + ['#', ' managed by Certbot'], + [], [] ]) def test_add_http_directives(self): @@ -174,8 +177,8 @@ class NginxParserTest(util.NginxTest): nparser.parsed[filep], [[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], - ['server_name', 'foobar.com'], - ['server_name', 'example.*'], + ['server_name', 'foobar.com'], ['#', ' managed by Certbot'], + ['server_name', 'example.*'], [] ]]]) self.assertRaises(errors.MisconfigurationError, nparser.add_server_directives, @@ -256,7 +259,6 @@ class NginxParserTest(util.NginxTest): ['ssl_session_timeout', '1440m'], ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], ['ssl_prefer_server_ciphers', 'on'], - ['#', ' Using list of ciphers from "Bulletproof SSL and TLS"'], ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-'+ 'AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256'+ '-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384'+ From fe76d558edcd493cc302060761426dc4b6cc0248 Mon Sep 17 00:00:00 2001 From: Yen Chi Hsuan Date: Sat, 6 Aug 2016 20:31:21 +0800 Subject: [PATCH 048/331] Enable unit tests of certbot core on Python 3 --- certbot/account.py | 3 ++- certbot/client.py | 5 ++--- certbot/crypto_util.py | 25 ++++++++++++++++--------- certbot/display/ops.py | 7 ++++++- certbot/display/util.py | 9 +++++---- certbot/plugins/disco.py | 2 +- certbot/plugins/disco_test.py | 3 ++- certbot/plugins/manual_test.py | 2 +- certbot/plugins/selection.py | 3 ++- certbot/plugins/util_test.py | 4 ++-- certbot/plugins/webroot.py | 2 +- certbot/reverter.py | 12 +++++++----- certbot/storage.py | 2 +- certbot/tests/account_test.py | 8 ++++---- certbot/tests/acme_util.py | 9 +++++---- certbot/tests/auth_handler_test.py | 17 +++++++++-------- certbot/tests/cli_test.py | 23 ++++++++++++----------- certbot/tests/client_test.py | 4 ++-- certbot/tests/configuration_test.py | 2 ++ certbot/tests/crypto_util_test.py | 4 ++-- certbot/tests/display/util_test.py | 24 ++++++++++++------------ certbot/tests/reverter_test.py | 4 ++-- certbot/tests/storage_test.py | 29 +++++++++++++++-------------- certbot/tests/util_test.py | 12 +++++++++--- certbot/util.py | 20 ++++++++++++++------ tox.ini | 6 ++++++ 26 files changed, 142 insertions(+), 99 deletions(-) diff --git a/certbot/account.py b/certbot/account.py index 798f8664e..71951d8e0 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -8,6 +8,7 @@ import socket from cryptography.hazmat.primitives import serialization import pyrfc3339 import pytz +import six import zope.component from acme import fields as acme_fields @@ -108,7 +109,7 @@ class AccountMemoryStorage(interfaces.AccountStorage): self.accounts = initial_accounts if initial_accounts is not None else {} def find_all(self): - return self.accounts.values() + return list(six.itervalues(self.accounts)) def save(self, account): if account.id in self.accounts: diff --git a/certbot/client.py b/certbot/client.py index 0f414b474..119fb0947 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -246,8 +246,7 @@ class Client(object): domains, self.config.allow_subset_of_names) - auth_domains = set(a.body.identifier.value.encode('ascii') - for a in authzr) + auth_domains = set(a.body.identifier.value for a in authzr) domains = [d for d in domains if d in auth_domains] # Create CSR from names @@ -317,7 +316,7 @@ class Client(object): self.config.strict_permissions) cert_pem = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) + OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode('ascii') cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 1e831dd8f..7253742b0 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -10,6 +10,7 @@ import traceback import OpenSSL import pyrfc3339 +import six import zope.component from acme import crypto_util as acme_crypto_util @@ -115,16 +116,16 @@ def make_csr(key_str, domains, must_staple=False): # TODO: put SAN if len(domains) > 1 extensions = [ OpenSSL.crypto.X509Extension( - "subjectAltName", + b"subjectAltName", critical=False, - value=", ".join("DNS:%s" % d for d in domains) + value=", ".join("DNS:%s" % d for d in domains).encode('ascii') ) ] if must_staple: extensions.append(OpenSSL.crypto.X509Extension( - "1.3.6.1.5.5.7.1.24", + b"1.3.6.1.5.5.7.1.24", critical=False, - value="DER:30:03:02:01:05")) + value=b"DER:30:03:02:01:05")) req.add_extensions(extensions) req.set_version(2) req.set_pubkey(pkey) @@ -350,7 +351,7 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): if isinstance(cert, jose.ComparableX509): # pylint: disable=protected-access cert = cert.wrapped - return OpenSSL.crypto.dump_certificate(filetype, cert) + return OpenSSL.crypto.dump_certificate(filetype, cert).decode('ascii') # assumes that OpenSSL.crypto.dump_certificate includes ending # newline character @@ -395,8 +396,14 @@ def _notAfterBefore(cert_path, method): with open(cert_path) as f: x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) + # pyopenssl always returns bytes timestamp = method(x509) - reformatted_timestamp = [timestamp[0:4], "-", timestamp[4:6], "-", - timestamp[6:8], "T", timestamp[8:10], ":", - timestamp[10:12], ":", timestamp[12:]] - return pyrfc3339.parse("".join(reformatted_timestamp)) + reformatted_timestamp = [timestamp[0:4], b"-", timestamp[4:6], b"-", + timestamp[6:8], b"T", timestamp[8:10], b":", + timestamp[10:12], b":", timestamp[12:]] + timestamp_str = b"".join(reformatted_timestamp) + # pyrfc3339 uses "native" strings. That is, bytes on Python 2 and unicode + # on Python 3 + if six.PY3: + timestamp_str = timestamp_str.decode('ascii') + return pyrfc3339.parse(timestamp_str) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index c7e566256..4db6d71e2 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -180,7 +180,12 @@ def _choose_names_manually(): try: domain_list[i] = util.enforce_domain_sanity(domain) except errors.ConfigurationError as e: - invalid_domains[domain] = e.message + try: # Python 2 + # pylint: disable=no-member + err_msg = e.message.encode('utf-8') + except AttributeError: + err_msg = str(e) + invalid_domains[domain] = err_msg if len(invalid_domains): retry_message = ( diff --git a/certbot/display/util.py b/certbot/display/util.py index 39486b2bd..e2624395c 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -4,6 +4,7 @@ import os import textwrap import dialog +import six import zope.interface from certbot import interfaces @@ -253,7 +254,7 @@ class FileDisplay(object): "{line}{frame}{line}{msg}{line}{frame}{line}".format( line=os.linesep, frame=side_frame, msg=message)) if pause: - raw_input("Press Enter to Continue") + six.moves.input("Press Enter to Continue") def menu(self, message, choices, ok_label="", cancel_label="", help_label="", **unused_kwargs): @@ -295,7 +296,7 @@ class FileDisplay(object): :rtype: tuple """ - ans = raw_input( + ans = six.moves.input( textwrap.fill( "%s (Enter 'c' to cancel): " % message, 80, @@ -330,7 +331,7 @@ class FileDisplay(object): os.linesep, frame=side_frame, msg=message)) while True: - ans = raw_input("{yes}/{no}: ".format( + ans = six.moves.input("{yes}/{no}: ".format( yes=_parens_around_char(yes_label), no=_parens_around_char(no_label))) @@ -468,7 +469,7 @@ class FileDisplay(object): input_msg = ("Press 1 [enter] to confirm the selection " "(press 'c' to cancel): ") while selection < 1: - ans = raw_input(input_msg) + ans = six.moves.input(input_msg) if ans.startswith("c") or ans.startswith("C"): return CANCEL, -1 try: diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index 59410757c..a6e8e7ed7 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -255,4 +255,4 @@ class PluginsRegistry(collections.Mapping): def __str__(self): if not self._plugins: return "No plugins" - return "\n\n".join(str(p_ep) for p_ep in self._plugins.itervalues()) + return "\n\n".join(str(p_ep) for p_ep in six.itervalues(self._plugins)) diff --git a/certbot/plugins/disco_test.py b/certbot/plugins/disco_test.py index cef6ede8f..509110d55 100644 --- a/certbot/plugins/disco_test.py +++ b/certbot/plugins/disco_test.py @@ -3,6 +3,7 @@ import unittest import mock import pkg_resources +import six import zope.interface from certbot import errors @@ -50,7 +51,7 @@ class PluginEntryPointTest(unittest.TestCase): EP_SA: "sa", } - for entry_point, name in names.iteritems(): + for entry_point, name in six.iteritems(names): self.assertEqual( name, PluginEntryPoint.entry_point_to_plugin_name(entry_point)) diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index af1dc9909..dd0905049 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -51,7 +51,7 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("certbot.plugins.manual.zope.component.getUtility") @mock.patch("certbot.plugins.manual.sys.stdout") @mock.patch("acme.challenges.HTTP01Response.simple_verify") - @mock.patch("__builtin__.raw_input") + @mock.patch("six.moves.input") def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction): mock_verify.return_value = True mock_interaction().yesno.return_value = True diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index b16515d8f..a7388c4e1 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -4,6 +4,7 @@ from __future__ import print_function import os import logging +import six import zope.component from certbot import errors @@ -78,7 +79,7 @@ def pick_plugin(config, default, plugins, question, ifaces): if len(prepared) > 1: logger.debug("Multiple candidate plugins: %s", prepared) - plugin_ep = choose_plugin(prepared.values(), question) + plugin_ep = choose_plugin(list(six.itervalues(prepared)), question) if plugin_ep is None: return None else: diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index a9466ed63..e1a064fb3 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -63,7 +63,7 @@ class AlreadyListeningTestNoPsutil(unittest.TestCase): def test_ports_available(self, mock_getutil): import certbot.plugins.util as plugins_util # Ensure we don't get error - with mock.patch("socket._socketobject.bind"): + with mock.patch("socket.socket.bind"): self.assertFalse(plugins_util.already_listening(80)) self.assertFalse(plugins_util.already_listening(80, True)) self.assertEqual(mock_getutil.call_count, 0) @@ -73,7 +73,7 @@ class AlreadyListeningTestNoPsutil(unittest.TestCase): sys.modules["psutil"] = None import certbot.plugins.util as plugins_util import socket - with mock.patch("socket._socketobject.bind", side_effect=socket.error): + with mock.patch("socket.socket.bind", side_effect=socket.error): self.assertTrue(plugins_util.already_listening(80)) self.assertTrue(plugins_util.already_listening(80, True)) with mock.patch("socket.socket", side_effect=socket.error): diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index 624ee2ff4..1cd1d879a 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -206,7 +206,7 @@ to serve all files under specified web root ({0}).""" old_umask = os.umask(0o022) try: - with open(validation_path, "w") as validation_file: + with open(validation_path, "wb") as validation_file: validation_file.write(validation.encode()) finally: os.umask(old_umask) diff --git a/certbot/reverter.py b/certbot/reverter.py index 1c404e29b..098c74911 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -7,7 +7,7 @@ import shutil import time import traceback - +import six import zope.component from certbot import constants @@ -310,7 +310,9 @@ class Reverter(object): def _run_undo_commands(self, filepath): # pylint: disable=no-self-use """Run all commands in a file.""" - with open(filepath, 'rb') as csvfile: + # NOTE: csv module uses native strings. That is, bytes on Python 2 and + # unicode on Python 3 + with open(filepath, 'r') as csvfile: csvreader = csv.reader(csvfile) for command in reversed(list(csvreader)): try: @@ -408,9 +410,9 @@ class Reverter(object): command_file = None try: if os.path.isfile(commands_fp): - command_file = open(commands_fp, "ab") + command_file = open(commands_fp, "a") else: - command_file = open(commands_fp, "wb") + command_file = open(commands_fp, "w") csvwriter = csv.writer(command_file) csvwriter.writerow(command) @@ -569,7 +571,7 @@ class Reverter(object): # It is possible save checkpoints faster than 1 per second resulting in # collisions in the naming convention. - for _ in xrange(2): + for _ in six.moves.range(2): timestamp = self._checkpoint_timestamp() final_dir = os.path.join(self.config.backup_dir, timestamp) try: diff --git a/certbot/storage.py b/certbot/storage.py index 82fdbfd54..5ca2ff6a9 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -90,7 +90,7 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data): # TODO: add human-readable comments explaining other available # parameters logger.debug("Writing new config %s.", n_filename) - with open(n_filename, "w") as f: + with open(n_filename, "wb") as f: config.write(outfile=f) return config diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index 4cd2bfebf..41b835838 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -131,8 +131,8 @@ class AccountFileStorageTest(unittest.TestCase): for file_name in "regr.json", "meta.json", "private_key.json": self.assertTrue(os.path.exists( os.path.join(account_path, file_name))) - self.assertEqual("0400", oct(os.stat(os.path.join( - account_path, "private_key.json"))[stat.ST_MODE] & 0o777)) + self.assertTrue(oct(os.stat(os.path.join( + account_path, "private_key.json"))[stat.ST_MODE] & 0o777) in ("0400", "0o400")) # restore self.assertEqual(self.acc, self.storage.load(self.acc.id)) @@ -179,14 +179,14 @@ class AccountFileStorageTest(unittest.TestCase): self.storage.save(self.acc) mock_open = mock.mock_open() mock_open.side_effect = IOError - with mock.patch("__builtin__.open", mock_open): + with mock.patch("six.moves.builtins.open", mock_open): self.assertRaises( errors.AccountStorageError, self.storage.load, self.acc.id) def test_save_ioerrors(self): mock_open = mock.mock_open() mock_open.side_effect = IOError # TODO: [None, None, IOError] - with mock.patch("__builtin__.open", mock_open): + with mock.patch("six.moves.builtins.open", mock_open): self.assertRaises( errors.AccountStorageError, self.storage.save, self.acc) diff --git a/certbot/tests/acme_util.py b/certbot/tests/acme_util.py index 3d33c5723..4f6e86cc7 100644 --- a/certbot/tests/acme_util.py +++ b/certbot/tests/acme_util.py @@ -1,6 +1,7 @@ """ACME utilities for testing.""" import datetime -import itertools + +import six from acme import challenges from acme import jose @@ -13,10 +14,10 @@ KEY = test_util.load_rsa_private_key('rsa512_key.pem') # Challenges HTTP01 = challenges.HTTP01( - token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") + token=b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") TLSSNI01 = challenges.TLSSNI01( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) -DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") +DNS = challenges.DNS(token=b"17817c66b60ce2e4012dfad92657527a") CHALLENGES = [HTTP01, TLSSNI01, DNS] @@ -62,7 +63,7 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True): # pylint: disable=redefined-outer-name challbs = tuple( chall_to_challb(chall, status) - for chall, status in itertools.izip(challs, statuses) + for chall, status in six.moves.zip(challs, statuses) ) authz_kwargs = { "identifier": messages.Identifier( diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index eccc36418..fce130f7c 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -4,6 +4,7 @@ import logging import unittest import mock +import six from acme import challenges from acme import client as acme_client @@ -93,7 +94,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertEqual(mock_poll.call_count, 1) chall_update = mock_poll.call_args[0][0] - self.assertEqual(chall_update.keys(), ["0"]) + self.assertEqual(list(six.iterkeys(chall_update)), ["0"]) self.assertEqual(len(chall_update.values()), 1) self.assertEqual(self.mock_auth.cleanup.call_count, 1) @@ -118,7 +119,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertEqual(mock_poll.call_count, 1) chall_update = mock_poll.call_args[0][0] - self.assertEqual(chall_update.keys(), ["0"]) + self.assertEqual(list(six.iterkeys(chall_update)), ["0"]) self.assertEqual(len(chall_update.values()), 1) self.assertEqual(self.mock_auth.cleanup.call_count, 1) @@ -143,12 +144,12 @@ class GetAuthorizationsTest(unittest.TestCase): # Check poll call self.assertEqual(mock_poll.call_count, 1) chall_update = mock_poll.call_args[0][0] - self.assertEqual(len(chall_update.keys()), 3) - self.assertTrue("0" in chall_update.keys()) + self.assertEqual(len(list(six.iterkeys(chall_update))), 3) + self.assertTrue("0" in list(six.iterkeys(chall_update))) self.assertEqual(len(chall_update["0"]), 1) - self.assertTrue("1" in chall_update.keys()) + self.assertTrue("1" in list(six.iterkeys(chall_update))) self.assertEqual(len(chall_update["1"]), 1) - self.assertTrue("2" in chall_update.keys()) + self.assertTrue("2" in list(six.iterkeys(chall_update))) self.assertEqual(len(chall_update["2"]), 1) self.assertEqual(self.mock_auth.cleanup.call_count, 1) @@ -167,7 +168,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, []) def _validate_all(self, unused_1, unused_2): - for dom in self.handler.authzr.keys(): + for dom in six.iterkeys(self.handler.authzr): azr = self.handler.authzr[dom] self.handler.authzr[dom] = acme_util.gen_authzr( messages.STATUS_VALID, @@ -317,7 +318,7 @@ class GenChallengePathTest(unittest.TestCase): """ def setUp(self): - logging.disable(logging.fatal) + logging.disable(logging.FATAL) def tearDown(self): logging.disable(logging.NOTSET) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index cb32975e4..8cd879144 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -136,7 +136,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods try: with mock.patch('certbot.main.sys.stderr'): main.main(self.standard_args + args[:]) # NOTE: parser can alter its args! - except errors.MissingCommandlineFlag as exc: + except errors.MissingCommandlineFlag as exc_: + exc = exc_ self.assertTrue(message in str(exc)) self.assertTrue(exc is not None) @@ -263,7 +264,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods flags = ['--init', '--prepare', '--authenticators', '--installers'] for args in itertools.chain( *(itertools.combinations(flags, r) - for r in xrange(len(flags)))): + for r in six.moves.range(len(flags)))): self._call(['plugins'] + list(args)) @mock.patch('certbot.main.plugins_disco') @@ -332,7 +333,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call(['-a', 'bad_auth', 'certonly']) assert False, "Exception should have been raised" except errors.PluginSelectionError as e: - self.assertTrue('The requested bad_auth plugin does not appear' in e.message) + self.assertTrue('The requested bad_auth plugin does not appear' in str(e)) def test_check_config_sanity_domain(self): # Punycode @@ -427,9 +428,9 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods "The following flags didn't conflict with " '--server: {0}'.format(', '.join(conflicting_args))) except errors.Error as error: - self.assertTrue('--server' in error.message) + self.assertTrue('--server' in str(error)) for arg in conflicting_args: - self.assertTrue(arg in error.message) + self.assertTrue(arg in str(error)) def test_must_staple_flag(self): parse = self._get_argument_parser() @@ -855,10 +856,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods server = 'foo.bar' self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY, '--server', server, 'revoke']) - with open(KEY) as f: + with open(KEY, 'rb') as f: mock_acme_client.Client.assert_called_once_with( server, key=jose.JWK.load(f.read()), net=mock.ANY) - with open(CERT) as f: + with open(CERT, 'rb') as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] mock_revoke = mock_acme_client.Client().revoke mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) @@ -885,7 +886,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods config.verbose_count = 1 main._handle_exception( Exception, exc_value=exception, trace=None, config=None) - mock_open().write.assert_called_once_with(''.join( + mock_open().write.assert_any_call(''.join( traceback.format_exception_only(Exception, exception))) error_msg = mock_sys.exit.call_args_list[0][0][0] self.assertTrue('unexpected error' in error_msg) @@ -935,8 +936,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertRaises( argparse.ArgumentTypeError, cli.read_file, rel_test_path) - test_contents = 'bar\n' - with open(rel_test_path, 'w') as f: + test_contents = b'bar\n' + with open(rel_test_path, 'wb') as f: f.write(test_contents) path, contents = cli.read_file(rel_test_path) @@ -1106,7 +1107,7 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): def test_find_duplicative_names(self, unused_makedir): from certbot.main import _find_duplicative_certs test_cert = test_util.load_vector('cert-san.pem') - with open(self.test_rc.cert, 'w') as f: + with open(self.test_rc.cert, 'wb') as f: f.write(test_cert) # No overlap at all diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index e7ae6bbd1..4a8a8bdee 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -232,11 +232,11 @@ class ClientTest(unittest.TestCase): self.assertEqual(os.path.dirname(fullchain_path), os.path.dirname(candidate_fullchain_path)) - with open(cert_path, "r") as cert_file: + with open(cert_path, "rb") as cert_file: cert_contents = cert_file.read() self.assertEqual(cert_contents, test_util.load_vector(certs[0])) - with open(chain_path, "r") as chain_file: + with open(chain_path, "rb") as chain_file: chain_contents = chain_file.read() self.assertEqual(chain_contents, test_util.load_vector(certs[1]) + test_util.load_vector(certs[2])) diff --git a/certbot/tests/configuration_test.py b/certbot/tests/configuration_test.py index 13d85bd9f..211a0eae6 100644 --- a/certbot/tests/configuration_test.py +++ b/certbot/tests/configuration_test.py @@ -60,6 +60,7 @@ class NamespaceConfigTest(unittest.TestCase): config_base = "foo" work_base = "bar" logs_base = "baz" + server = "mock.server" mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', 'logs_dir', 'http01_port', @@ -68,6 +69,7 @@ class NamespaceConfigTest(unittest.TestCase): mock_namespace.config_dir = config_base mock_namespace.work_dir = work_base mock_namespace.logs_dir = logs_base + mock_namespace.server = server config = NamespaceConfig(mock_namespace) self.assertTrue(os.path.isabs(config.config_dir)) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 5a592bbb1..c0dc1de3a 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -111,7 +111,7 @@ class MakeCSRTest(unittest.TestCase): # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID, # and the shortname field is just "UNDEF" must_staple_exts = [e for e in csr.get_extensions() - if e.get_data() == "0\x03\x02\x01\x05"] + if e.get_data() == b"0\x03\x02\x01\x05"] self.assertEqual(len(must_staple_exts), 1, "Expected exactly one Must Staple extension") @@ -341,7 +341,7 @@ class CertLoaderTest(unittest.TestCase): def test_load_invalid_cert(self): from certbot.crypto_util import pyopenssl_load_certificate - bad_cert_data = CERT.replace("BEGIN CERTIFICATE", "ASDFASDFASDF!!!") + bad_cert_data = CERT.replace(b"BEGIN CERTIFICATE", b"ASDFASDFASDF!!!") self.assertRaises( errors.Error, pyopenssl_load_certificate, bad_cert_data) diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index a6ced90ab..0462305bd 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -151,7 +151,7 @@ class FileOutputDisplayTest(unittest.TestCase): self.assertTrue("message" in string) def test_notification_pause(self): - with mock.patch("__builtin__.raw_input", return_value="enter"): + with mock.patch("six.moves.input", return_value="enter"): self.displayer.notification("message") self.assertTrue("message" in self.mock_stdout.write.call_args[0][0]) @@ -164,31 +164,31 @@ class FileOutputDisplayTest(unittest.TestCase): self.assertEqual(ret, (display_util.OK, 0)) def test_input_cancel(self): - with mock.patch("__builtin__.raw_input", return_value="c"): + with mock.patch("six.moves.input", return_value="c"): code, _ = self.displayer.input("message") self.assertTrue(code, display_util.CANCEL) def test_input_normal(self): - with mock.patch("__builtin__.raw_input", return_value="domain.com"): + with mock.patch("six.moves.input", return_value="domain.com"): code, input_ = self.displayer.input("message") self.assertEqual(code, display_util.OK) self.assertEqual(input_, "domain.com") def test_yesno(self): - with mock.patch("__builtin__.raw_input", return_value="Yes"): + with mock.patch("six.moves.input", return_value="Yes"): self.assertTrue(self.displayer.yesno("message")) - with mock.patch("__builtin__.raw_input", return_value="y"): + with mock.patch("six.moves.input", return_value="y"): self.assertTrue(self.displayer.yesno("message")) - with mock.patch("__builtin__.raw_input", side_effect=["maybe", "y"]): + with mock.patch("six.moves.input", side_effect=["maybe", "y"]): self.assertTrue(self.displayer.yesno("message")) - with mock.patch("__builtin__.raw_input", return_value="No"): + with mock.patch("six.moves.input", return_value="No"): self.assertFalse(self.displayer.yesno("message")) - with mock.patch("__builtin__.raw_input", side_effect=["cancel", "n"]): + with mock.patch("six.moves.input", side_effect=["cancel", "n"]): self.assertFalse(self.displayer.yesno("message")) - with mock.patch("__builtin__.raw_input", return_value="a"): + with mock.patch("six.moves.input", return_value="a"): self.assertTrue(self.displayer.yesno("msg", yes_label="Agree")) @mock.patch("certbot.display.util.FileDisplay.input") @@ -275,11 +275,11 @@ class FileOutputDisplayTest(unittest.TestCase): def test_get_valid_int_ans_valid(self): # pylint: disable=protected-access - with mock.patch("__builtin__.raw_input", return_value="1"): + with mock.patch("six.moves.input", return_value="1"): self.assertEqual( self.displayer._get_valid_int_ans(1), (display_util.OK, 1)) ans = "2" - with mock.patch("__builtin__.raw_input", return_value=ans): + with mock.patch("six.moves.input", return_value=ans): self.assertEqual( self.displayer._get_valid_int_ans(3), (display_util.OK, int(ans))) @@ -292,7 +292,7 @@ class FileOutputDisplayTest(unittest.TestCase): ["c"], ] for ans in answers: - with mock.patch("__builtin__.raw_input", side_effect=ans): + with mock.patch("six.moves.input", side_effect=ans): self.assertEqual( self.displayer._get_valid_int_ans(3), (display_util.CANCEL, -1)) diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index 450cecacf..62a43f0fe 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -1,6 +1,5 @@ """Test certbot.reverter.""" import csv -import itertools import logging import os import shutil @@ -8,6 +7,7 @@ import tempfile import unittest import mock +import six from certbot import errors @@ -153,7 +153,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): act_coms = get_undo_commands(self.config.temp_checkpoint_dir) - for a_com, com in itertools.izip(act_coms, coms): + for a_com, com in six.moves.zip(act_coms, coms): self.assertEqual(a_com, com) def test_bad_register_undo_command(self): diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 261500b98..7ac7771da 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -9,6 +9,7 @@ import unittest import configobj import mock import pytz +import six import certbot from certbot import cli @@ -92,8 +93,8 @@ class BaseRenewableCertTest(unittest.TestCase): os.symlink(os.path.join(os.path.pardir, os.path.pardir, "archive", "example.org", "{0}{1}.pem".format(kind, ver)), link) - with open(link, "w") as f: - f.write(kind if value is None else value) + with open(link, "wb") as f: + f.write(kind.encode('ascii') if value is None else value) def _write_out_ex_kinds(self): for kind in ALL_FOUR: @@ -235,7 +236,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.current_version("cert"), None) def test_latest_and_next_versions(self): - for ver in xrange(1, 6): + for ver in six.moves.range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.assertEqual(self.test_rc.latest_common_version(), 5) @@ -258,7 +259,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.next_free_version(), 18) def test_update_link_to(self): - for ver in xrange(1, 6): + for ver in six.moves.range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) @@ -285,12 +286,12 @@ class RenewableCertTests(BaseRenewableCertTest): os.path.basename(self.test_rc.version("cert", 8))) def test_update_all_links_to_success(self): - for ver in xrange(1, 6): + for ver in six.moves.range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) self.assertEqual(self.test_rc.latest_common_version(), 5) - for ver in xrange(1, 6): + for ver in six.moves.range(1, 6): self.test_rc.update_all_links_to(ver) for kind in ALL_FOUR: self.assertEqual(ver, self.test_rc.current_version(kind)) @@ -330,11 +331,11 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.current_version(kind), 11) def test_has_pending_deployment(self): - for ver in xrange(1, 6): + for ver in six.moves.range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) - for ver in xrange(1, 6): + for ver in six.moves.range(1, 6): self.test_rc.update_all_links_to(ver) for kind in ALL_FOUR: self.assertEqual(ver, self.test_rc.current_version(kind)) @@ -373,10 +374,10 @@ class RenewableCertTests(BaseRenewableCertTest): self._write_out_ex_kinds() self.test_rc.update_all_links_to(12) - with open(self.test_rc.cert, "w") as f: + with open(self.test_rc.cert, "wb") as f: f.write(test_cert) self.test_rc.update_all_links_to(11) - with open(self.test_rc.cert, "w") as f: + with open(self.test_rc.cert, "wb") as f: f.write(test_cert) mock_datetime.timedelta = datetime.timedelta @@ -426,7 +427,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(self.test_rc.should_autodeploy()) self.test_rc.configuration["autodeploy"] = "1" # No pending deployment - for ver in xrange(1, 6): + for ver in six.moves.range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.assertFalse(self.test_rc.should_autodeploy()) @@ -461,7 +462,7 @@ class RenewableCertTests(BaseRenewableCertTest): # (to avoid instantiating parser) mock_rv.side_effect = lambda x: x - for ver in xrange(1, 6): + for ver in six.moves.range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.test_rc.update_all_links_to(3) @@ -492,7 +493,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.version("privkey", i)))) for kind in ALL_FOUR: - self.assertEqual(self.test_rc.available_versions(kind), range(1, 9)) + self.assertEqual(self.test_rc.available_versions(kind), list(six.moves.range(1, 9))) self.assertEqual(self.test_rc.current_version(kind), 3) # Test updating from latest version rather than old version self.test_rc.update_all_links_to(8) @@ -501,7 +502,7 @@ class RenewableCertTests(BaseRenewableCertTest): "attempt", self.cli_config)) for kind in ALL_FOUR: self.assertEqual(self.test_rc.available_versions(kind), - range(1, 10)) + list(six.moves.range(1, 10))) self.assertEqual(self.test_rc.current_version(kind), 8) with open(self.test_rc.version("fullchain", 9)) as f: self.assertEqual(f.read(), "last" + "attempt") diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 36676443a..4aa6d3ff3 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -189,6 +189,12 @@ class UniqueFileTest(unittest.TestCase): self.assertTrue(basename3.endswith("foo.txt")) +try: + file_type = file +except NameError: + import io + file_type = io.TextIOWrapper + class UniqueLineageNameTest(unittest.TestCase): """Tests for certbot.util.unique_lineage_name.""" @@ -204,13 +210,13 @@ class UniqueLineageNameTest(unittest.TestCase): def test_basic(self): f, path = self._call("wow") - self.assertTrue(isinstance(f, file)) + self.assertTrue(isinstance(f, file_type)) self.assertEqual(os.path.join(self.root_path, "wow.conf"), path) def test_multiple(self): - for _ in xrange(10): + for _ in six.moves.range(10): f, name = self._call("wow") - self.assertTrue(isinstance(f, file)) + self.assertTrue(isinstance(f, file_type)) self.assertTrue(isinstance(name, str)) self.assertTrue("wow-0009.conf" in name) diff --git a/certbot/util.py b/certbot/util.py index 998808be0..e78ae664c 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -402,18 +402,27 @@ def enforce_domain_sanity(domain): :returns: The domain cast to `str`, with ASCII-only contents :rtype: str """ + if isinstance(domain, six.text_type): + wildcard_marker = u"*." + punycode_marker = u"xn--" + else: + wildcard_marker = b"*." + punycode_marker = b"xn--" + # Check if there's a wildcard domain - if domain.startswith("*."): + if domain.startswith(wildcard_marker): raise errors.ConfigurationError( "Wildcard domains are not supported: {0}".format(domain)) # Punycode - if "xn--" in domain: + if punycode_marker in domain: raise errors.ConfigurationError( "Punycode domains are not presently supported: {0}".format(domain)) # Unicode try: - domain = domain.encode('ascii').lower() + if isinstance(domain, six.binary_type): + domain = domain.decode('utf-8') + domain.encode('ascii') except UnicodeError: error_fmt = (u"Internationalized domain names " "are not presently supported: {0}") @@ -422,11 +431,10 @@ def enforce_domain_sanity(domain): else: raise errors.ConfigurationError(str(error_fmt).format(domain)) - if six.PY3: - domain = domain.decode('ascii') + domain = domain.lower() # Remove trailing dot - domain = domain[:-1] if domain.endswith('.') else domain + domain = domain[:-1] if domain.endswith(u'.') else domain # Explain separately that IP addresses aren't allowed (apart from not # being FQDNs) because hope springs eternal concerning this point diff --git a/tox.ini b/tox.ini index 27979d9df..9b0045d54 100644 --- a/tox.ini +++ b/tox.ini @@ -40,16 +40,22 @@ deps = commands = pip install -e acme[dns,dev] nosetests -v acme + pip install -e .[dev] + nosetests -v certbot [testenv:py34] commands = pip install -e acme[dns,dev] nosetests -v acme + pip install -e .[dev] + nosetests -v certbot [testenv:py35] commands = pip install -e acme[dns,dev] nosetests -v acme + pip install -e .[dev] + nosetests -v certbot [testenv:cover] basepython = python2.7 From 7deb1f0ad625ddd2d3603bcd8858ad674ee27fae Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Aug 2016 12:15:18 -0700 Subject: [PATCH 049/331] Fix bug with UnpsacedList.insert to final position - which only applied when the list actually contained spaces --- certbot-nginx/certbot_nginx/nginxparser.py | 5 +++-- .../certbot_nginx/tests/nginxparser_test.py | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 50cd15c29..7e2cd533a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -227,8 +227,8 @@ class UnspacedList(list): def insert(self, i, x): item, spaced_item = self._coerce(x) - self.spaced.insert(self._spaced_position(i) if i < len(self) else i, - spaced_item) + 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 @@ -292,6 +292,7 @@ class UnspacedList(list): # 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 diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 5fa9a7d1e..5c8d6d215 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -223,6 +223,26 @@ class TestUnspacedList(unittest.TestCase): self.assertRaises(IndexError, self.ul2.__getitem__, 2) self.assertRaises(IndexError, self.ul2.__getitem__, -3) + def test_insert(self): + x = UnspacedList( + [['\n ', 'listen', ' ', '69.50.225.155:9000'], + ['\n ', 'listen', ' ', '127.0.0.1'], + ['\n ', 'server_name', ' ', '.example.com'], + ['\n ', 'server_name', ' ', 'example.*'], '\n', + ['listen', ' ', '5001 ssl']]) + x.insert(5, "FROGZ") + self.assertEqual(x, + [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], + ['server_name', '.example.com'], ['server_name', 'example.*'], + ['listen', '5001 ssl'], 'FROGZ']) + self.assertEqual(x.spaced, + [['\n ', 'listen', ' ', '69.50.225.155:9000'], + ['\n ', 'listen', ' ', '127.0.0.1'], + ['\n ', 'server_name', ' ', '.example.com'], + ['\n ', 'server_name', ' ', 'example.*'], '\n', + ['listen', ' ', '5001 ssl'], + 'FROGZ']) + def test_rawlists(self): ul3 = copy.deepcopy(self.ul) ul3.insert(0, "some") From da7e429125c7100653e0479fb5ecaf459e9a55d2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Aug 2016 15:14:06 -0700 Subject: [PATCH 050/331] Work around horrible spaciness API usage bug --- certbot-nginx/certbot_nginx/configurator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 0a8d2a88a..afa8b8087 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -338,11 +338,16 @@ class NginxConfigurator(common.Plugin): """ snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() + options_subblock = self.parser.loc["ssl_options"] + # the options file doesn't have a newline at the beginning, but there + # needs to be one when it's dropped into the file + if "\n" not in options_subblock[0]: + options_subblock[0].insert(0, "\n") ssl_block = ( [['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)], ['\n ', 'ssl_certificate', ' ', snakeoil_cert], ['\n ', 'ssl_certificate_key', ' ', snakeoil_key]] + - self.parser.loc["ssl_options"]) + options_subblock) self.parser.add_server_directives( vhost.filep, vhost.names, ssl_block, replace=False) From 262eb778fe54890e4d36c2093a7a836ac3603b8b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Aug 2016 15:29:23 -0700 Subject: [PATCH 051/331] Update Apache plugin supported OSes --- docs/using.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index f43fd15c5..15658b2c5 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -20,8 +20,10 @@ but for most users who want to avoid running an ACME client as root, either `letsencrypt-nosudo `_ or `simp_le `_ are more appropriate choices. -The Apache plugin currently requires a Debian-based OS with augeas version -1.0; this includes Ubuntu 12.04+ and Debian 7+. +The Apache plugin currently requires OS with augeas version 1.0; currently `it +supports +`_ +modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin. Getting Certbot From f0c2ed305958a8354077090d98ffb2b2bd6d191e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Aug 2016 15:45:49 -0700 Subject: [PATCH 052/331] Lint, improve coverage, rm unused code --- certbot-nginx/certbot_nginx/parser.py | 16 +--------------- .../certbot_nginx/tests/parser_test.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 536602e27..cf1f3c1db 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -540,7 +540,7 @@ def _comment_directive(block, location): if isinstance(next_entry, list): if "Certbot" in next_entry[-1]: return - next_entry = next_entry.spaced[0] + next_entry = next_entry.spaced[0] # pylint: disable=no-member block.insert(location + 1, COMMENT[:]) if "\n" not in next_entry: block.insert(location + 2, '\n') @@ -584,17 +584,3 @@ def _add_directive(block, directive, replace): 'tried to insert directive "{0}" but found ' 'conflicting "{1}".'.format(directive, block[location])) - -def _comment_spaced_block(block): - """Adds a "managed by Certbot" comment to every directive.""" - comment = " # managed by Certbot" - indent = 80 - len(comment) - for i, entry in enumerate(block): - if isinstance(entry, list): - line = "".join(entry) - line = "".join(c for c in line if c != "\n") - linelength = len(line) - extra = indent - linelength - if extra < 0: - extra = 0 - block[i][-1] += extra * " " + comment diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 09b1cfd0b..963b4c815 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -219,6 +219,23 @@ class NginxParserTest(util.NginxTest): self.assertEqual(winner, parser.get_best_match(target_name, names[i])) + def test_comment_directive(self): + # pylint: disable=protected-access + block = nginxparser.UnspacedList([ + ["\n", "a", " ", "b", "\n"], + ["c", " ", "d"], + ["\n", "e", " ", "f"]]) + from certbot_nginx.parser import _comment_directive, COMMENT + _comment_directive(block, 1) + _comment_directive(block, 0) + self.assertEqual(block.spaced, [ + ["\n", "a", " ", "b", "\n"], + COMMENT, + "\n", + ["c", " ", "d"], + COMMENT, + ["\n", "e", " ", "f"]]) + def test_get_all_certs_keys(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) filep = nparser.abs_path('sites-enabled/example.com') From e77a3ed7b968470c908960fb45079c9a2e9e8708 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Aug 2016 17:22:53 -0700 Subject: [PATCH 053/331] Return individual key, not entire config dictionary! --- certbot-nginx/certbot_nginx/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/constants.py b/certbot-nginx/certbot_nginx/constants.py index 453266878..98f924b2b 100644 --- a/certbot-nginx/certbot_nginx/constants.py +++ b/certbot-nginx/certbot_nginx/constants.py @@ -22,4 +22,4 @@ def os_constant(key): # based on what OS we are running under. To see an # approach to how to handle different OSes, see the # apache version of this file. - return CLI_DEFAULTS + return CLI_DEFAULTS[key] From d41ceff86d22e8ec774771fdd4ab0ee093cb9b64 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Aug 2016 17:24:54 -0700 Subject: [PATCH 054/331] Various WIP on nginx compatibility test --- .../configurators/nginx/common.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index 9fbb538e8..aff9d9467 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -75,12 +75,13 @@ class Proxy(configurators_common.Proxy): for k in constants.CLI_DEFAULTS.keys(): setattr(self.le_config, "nginx_" + k, constants.os_constant(k)) - # An alias - self.le_config.nginx_handle_modules = self.le_config.nginx_handle_mods + # This does not appear to exist in nginx (yet?) + # self.le_config.nginx_handle_modules = self.le_config.nginx_handle_mods + conf=configuration.NamespaceConfig(self.le_config) + zope.component.provideUtility(conf) self._nginx_configurator = configurator.NginxConfigurator( - config=configuration.NamespaceConfig(self.le_config), - name="nginx") + config=conf, name="nginx") self._nginx_configurator.prepare() def cleanup_from_tests(self): @@ -118,7 +119,7 @@ def _get_server_root(config): if os.path.isdir(os.path.join(config, name))] if len(subdirs) != 1: - errors.Error("Malformed configuration directory {0}".format(config)) + raise errors.Error("Malformed configuration directory {0}".format(config)) return os.path.join(config, subdirs[0].rstrip()) From 0504882e083252f2ed820bb96586235fdebd09d0 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Aug 2016 17:50:20 -0700 Subject: [PATCH 055/331] Always newline config edits Even if they're transient --- certbot-nginx/certbot_nginx/tls_sni_01.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index ebc92f5e3..c7ec80931 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -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 = ['\n', 'include', ' ', self.challenge_conf] root = self.configurator.parser.loc["root"] - bucket_directive = ['server_names_hash_bucket_size', ' ', '128'] + bucket_directive = ['\n', 'server_names_hash_bucket_size', ' ', '128'] main = self.configurator.parser.parsed[root] for key, body in main: From 7d27c1f50020a3594b5bc05c0db023139aec8578 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Aug 2016 17:51:55 -0700 Subject: [PATCH 056/331] More correct parsing of lines containing trailing space --- .../certbot_compatibility_test/configurators/nginx/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index aff9d9467..8e2899933 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -131,7 +131,7 @@ def _get_names(config): for this_file in files: for line in open(os.path.join(root, this_file)): if line.strip().startswith("server_name"): - names = line.partition("server_name")[2].rstrip(";") + names = line.partition("server_name")[2].rpartition(";")[0] [all_names.add(n) for n in names.split()] non_ip_names = set(n for n in all_names if not util.IP_REGEX.match(n)) return all_names, non_ip_names From 712bd9ee6b06d0964f53418fe7cb338d35f59ed1 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Aug 2016 17:58:22 -0700 Subject: [PATCH 057/331] Copy nginx options file into integration testing environment --- certbot-nginx/tests/boulder-integration.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot-nginx/tests/boulder-integration.sh b/certbot-nginx/tests/boulder-integration.sh index bd35aee21..53dddf7e8 100755 --- a/certbot-nginx/tests/boulder-integration.sh +++ b/certbot-nginx/tests/boulder-integration.sh @@ -7,6 +7,7 @@ export PATH="/usr/sbin:$PATH" # /usr/sbin/nginx nginx_root="$root/nginx" mkdir $nginx_root root="$nginx_root" ./certbot-nginx/tests/boulder-integration.conf.sh > $nginx_root/nginx.conf +cp certbot-nginx/certbot_nginx/options-ssl-nginx.conf $nginx_root killall nginx || true nginx -c $nginx_root/nginx.conf From 6e86c71259bfd0128f9d002aaec801768637df1f Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 8 Aug 2016 18:03:07 -0700 Subject: [PATCH 058/331] Provide a copy of the self-signed cert as the fullchain as well --- .../certbot_compatibility_test/test_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 3d03f7771..b5e023f36 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -144,7 +144,7 @@ def test_deploy_cert(plugin, temp_dir, domains): for domain in domains: try: - plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path) + plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path, cert_path) plugin.save() # Needed by the Apache plugin except le_errors.Error as error: logger.error("Plugin failed to deploy ceritificate for %s:", domain) From b5fa0fbad758ce4680631599de12557f602692cf Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Aug 2016 18:08:11 -0700 Subject: [PATCH 059/331] This is reportedly the correct magic --- certbot-nginx/certbot_nginx/configurator.py | 2 +- certbot-nginx/certbot_nginx/parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index afa8b8087..5e415bce6 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -341,7 +341,7 @@ class NginxConfigurator(common.Plugin): options_subblock = self.parser.loc["ssl_options"] # the options file doesn't have a newline at the beginning, but there # needs to be one when it's dropped into the file - if "\n" not in options_subblock[0]: + if options_subblock and "\n" not in options_subblock[0]: options_subblock[0].insert(0, "\n") ssl_block = ( [['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)], diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index cf1f3c1db..73224ea1f 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -179,7 +179,7 @@ class NginxParser(object): with open(ssl_options) as _file: return nginxparser.load(_file).spaced except IOError: - logger.debug("Could not open file: %s", ssl_options) + logger.warn("Missing NGINX TLS options file: %s", ssl_options) except pyparsing.ParseException: logger.debug("Could not parse file: %s", ssl_options) return [] From 9c168017aeccfc28b2a4f22c321753108794f75b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Aug 2016 18:17:02 -0700 Subject: [PATCH 060/331] That was not the correct magic --- certbot-nginx/tests/boulder-integration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/tests/boulder-integration.sh b/certbot-nginx/tests/boulder-integration.sh index 53dddf7e8..58613d86f 100755 --- a/certbot-nginx/tests/boulder-integration.sh +++ b/certbot-nginx/tests/boulder-integration.sh @@ -7,7 +7,7 @@ export PATH="/usr/sbin:$PATH" # /usr/sbin/nginx nginx_root="$root/nginx" mkdir $nginx_root root="$nginx_root" ./certbot-nginx/tests/boulder-integration.conf.sh > $nginx_root/nginx.conf -cp certbot-nginx/certbot_nginx/options-ssl-nginx.conf $nginx_root +cp certbot-nginx/certbot_nginx/options-ssl-nginx.conf "$root"/conf killall nginx || true nginx -c $nginx_root/nginx.conf From 3591667d026c32fd92d2aa9440f98b007ee3f578 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Wed, 10 Aug 2016 10:43:54 +0300 Subject: [PATCH 061/331] Fix tox tests --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 27979d9df..b53e598f8 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,7 @@ deps = py{26,27}-oldest: psutil==2.1.0 py{26,27}-oldest: PyOpenSSL==0.13 py{26,27}-oldest: python2-pythondialog==3.2.2rc1 + py{26,27}-oldest: dnspython>=1.12 [testenv:py33] commands = From cb9921f4b1c919ba337aca90309fe8a958033f4b Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 10 Aug 2016 11:14:39 -0700 Subject: [PATCH 062/331] Add more ignored files to gitignore. --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b653cb06c..48ec7910b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,11 @@ letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 /.vagrant +tags + # editor temporary files *~ -*.swp +*.sw? \#*# .idea From 595e51551866d39593e0e61f2a0dcbe7fa7ad844 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 10 Aug 2016 14:57:44 -0700 Subject: [PATCH 063/331] Restart web servers before beginning tests --- .../certbot_compatibility_test/configurators/apache/common.py | 2 +- .../certbot_compatibility_test/configurators/nginx/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 0c53058de..4e612bbd5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -58,7 +58,7 @@ class Proxy(configurators_common.Proxy): self._prepare_configurator() try: - subprocess.check_call("apachectl -k start".split()) + subprocess.check_call("apachectl -k restart".split()) except errors.Error: raise errors.Error( "Apache failed to load {0} before tests started".format( diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index 8e2899933..0d72605b7 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -62,7 +62,7 @@ class Proxy(configurators_common.Proxy): self._prepare_configurator() try: - subprocess.check_call("nginx".split()) + subprocess.check_call("service nginx reload".split()) except errors.Error: raise errors.Error( "Nginx failed to load {0} before tests started".format( From 4c596311b068974fd090495be8355323d711c440 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 15:39:35 -0700 Subject: [PATCH 064/331] Add nginx compatibility test data tarball --- .../testdata/nginx.tar.gz | Bin 0 -> 6625 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..4ca5cc9775a35f64534f90b1db4fb7ef272672a8 GIT binary patch literal 6625 zcmV<786M^ziwFQ)A*ojY1MFRGbK5wQp0CueK*dvgd$%K5?-IvXS4C1};@c$l+RDs% zzNlytk{I)C2+ESPwg3IP0m>vG((-O{p4}Ux%CXY@paJxQ7lMG=tBksNJ6;9xuI(l9 zGFY8{_pMVG(quH^zago=`R87LGV1qx{r+(HUAH&r4+r0o(KoM2P1>;B%?SA}OOpI4 z+%f&XXj&ce&+^T3`7L|F-_QSWFzLVY{}QQ@|GOk;Z}R20C<6%|Bd_)RkB0pb^530I zx*c8cx&Qjl-D7Mi)wbj#QBmuBpVV_>hIgY zYm>vjZsuc&KadThyx1(bJ>A5Cm-w{Ld;h+}gCXvVnB~;dO}m8H{b}E@$Qd~ybdx1Qp^vztYxKF|7OF(^LS+zQ^^Gnl2&n!L-?H?qP}!(5 zCwo;C_-5pA@L@nSwdK_iVMB)RJYL( z_tjg0<-VHQ3bw{up9Cd)BXm7%*`tSr&K0UYSIMm23Kj2v zQ@QixOz61pO}YR1LN(&5kz5ffbi6-K0P|di?l2nmU*<3Y~RIyMwTqP4z%o?*U*Ey0d6{;R@ zw5)m%q|N$VC-a#Jl^{*#Q*bupbH=nU^C?s#u9BTE`p%3G9#ifGUxf2#f-~9Ty-*3x zWMA=q&&LUe=o}(@`a!4!b@G(-MW{@kOD3EZ^|824W zPVOsfTxH4;Z*v|Ebrj!x5?Vg|4(0dZC27Fs?Cb#(%q2_+wf z`be#kEwr3fL(S?4C8t#%C`vJ5*;AfVwfpCHLd%DvK2@}0g0(rTx~?eyTPXRE)D6|H zQ1U^kn~L(iQ1W4^TZ;07Q1XGP&lKfHq2xnTS82Ntq&l2b-BGlcLdiKbROO8z)#0QX zD&to|DM(cr7o0krQ=_jkE|i>8LnTJ^GKW*Dt9|7cL5w3fRqg&Hl!8;$gYs~7IJJh# z!HrM~YSp##7op_T8fvGwE^5ZPHB^Q~KXo{_hT3JW-&sXwPOqVsyVr7l4Yf~|gZFQ( z-}<5a1ykexkBd4e`sy-{?Em`X@mSgajR%v_>;CU066}XEvfXamzMH#et!N>eus?&%^ABARS+G9DrBA*F1wcG()XZ@UExzEINd4n@_V<*?^mI_DmGd^sVN^zEj~=?det+N}79vY|;w2#?^Y_>HJ@Id6 z|GSUdkXe?bDelX`#Qoq0lF{XlW?7Qep}0bT_9eaj5~OM|E0l4H@zcT&n0pJo;!VT? zI({P<;~KDD^o2qI-q{8CDjpusfEfag zEj64+lG9y&nuf4{6CXXsBr-oU$R`QhdzX1x07HRi*hQhFulfI4G?!cl^`;;IsN!ErbO*Wo;UxlL5(xYwIL(gAfMn33f$F33HO}ySG6YbEWc}~;CPQ`oA9p9O>;FrnR>?CV zSN2~%+1K;M2YdeRV#Yh&_Zj_lLs`CvXueKh*{{QYx;iICdc<$CKn4T!>Sl30zdHZ; zb`J(<_OO8E4rY8AZC!kv+aKqP`OT%hN8@{2m^k?C3v*xf&OqqLFABer>&dY+sRj3c|kn$-Y`#UR}&Dtc&;DV6mj`2X(#YuB~_W2hqUq z8O(wam2vOp^UG_w=cCSC`HC_UM4Nh@}4;#bm7SAQ|$o+%sQ9p?*=k^_~ z=5!tpaI1$2L#pV(lB@GLsn=GTT_aLgF86yo(@Q)neYsMH&j0Nl#Uya7( z)&=5?2`VNklmnAe4$u#$5@{1js z*+W^;{xsaMb`-=LR#d?3qt$WqO%2T)Ai?9N8!ehSMCy7^-cr%b5msU(KvCfB?{8=( zGHMpq(;x)RcCSKC&53eVM>D5bAE~?t80V(w=K5H@BBGfFW*9c!+|f+r)GRpVqw#s9 zLQV%wPZ~@IfUvq69!0As(VEpmxza05kRu;@Up+O{tO3dg>It!C4N6$;_MyaUAq6-xj51>`t>aF&!0m^DjBxvriqYZeY7Tpr|OWz8~CZmK8Bnq{Hf zQV)|gYliZfdXB7FHp-Q`35K(U<3o4U!{Zi6wOQ9tD7XSSQeY?)T!S1bFcb>rAV&%e zg#wrXXGnpeP~d9HJ<*A?fc1Qf7?m3(dnioUv$!JbhSCFN`U|y z9c@Z0F3f*6df9a3i%l)f>EcQ*xfqs9hp+FFi{Ww0!KHjExfmWc9Gw5Bl8ZMW9imin z@e-sXlu9nX17A906iY77vAhY2B^P^$#tD8Z`4}H>9392FEarHe-=~tFE8W~NQ7ifT z0i+g6C4X1CxHChoVxCCbn7zMP6uI5wn4ro?c#N8dzoY&Z9_5b{l$|* zKg`arYzz3G8}m<`-F~-QbL7%E|CjewzIu*1GXGCTWA*z#<6(dBI{&{!YKf~Xf8|#r z`8|_$9);wo!~yURJlVjfvoa^%e|a(ipaS0=DTnO#)`Q4h(bE--il-9jtMJb$T|EWo z`p@Y~7q#TzPO0=M9O!01%4m< z9kz)(pZhS*p?+O#PlVp)J zURBA>gXNQ*0rdDIb%@6%+(v0_3qW4Q@7e*(h|OhyR)&9mvJIc*7VO#)Yd6#clTsOW zziQ|;B7q)%zEtyU>20HVOsFqNsCLjLJXy83NsWWI$=;*taA~`lw+`;9(qx!fx9~&V ztR@?4axQ8P@saDbZ-aPEcf+oXU;nzRfwOG_`>UD+4_k-u)Nkrfps9f;EU%v;;IWCq zj~mpC{Jg0Q&>X_CzvBOCCxWSJla=I0gUz%a4}FU6JgW4QU05@36>wxbx&4fnSNDk~ zc_Tbcwlw1}9?KS|4e(;V@Z`#%q#nOYk`+ANKyS&{ox3O$9sX`Vu^fSa5$55;A$-@K zeBm=vhPRrly<$>}_TZV?t>MYr4|y409RY2>oSLBj`$651DYATy!?ewBIX@;8)!`%e zjxrh^bf9cgGgNl%lxDSaMH-T{W=(<&L73D{)oJFz2)M1wlDS!}PqVtwzQae=H9Tsj zu6GBM6ErA>U$E9>^%Y{YjsTeGs9OtPC*IxG1^+KYm#s_qw$}ap88@~)kG z^)oNZ^SinnyHQtfdv4pKnN*4oO-^Y-q$mnw6t^dtJuE#7*dq5{Bu^x z6Q%uz2V#+OCZmT>fx$aN8Gg)}ppW)D9+BtQP2k<(cV?RMB==P~JTgWNNS;t%#qVG` zzJCG_S{WVMe=&z>;ucrvFrwG5FQ-s&_ zubLXy|ILOq+A8Lctb=vv=2ft^v5jk59s2dV zfxmTXod5Y3&tGKz@Ak*)_rE7y`1Lyfzd#Z+&T4U5;K#5SxgB3nEI;|lJlp@nQc9romDy`kE$jgYdcCR8X zqtQC!intPq)~*s-MxnJw)k4b%v`!UXMxV8-U8{^d>r|~&qR!gYPFzNuwe^a)j5cdm zyLK6A);25RGRmxNRm5e4S$kFym(gYIs&gxmW$o%vD5J_cRdFSvtW)Kpj3(<;xhNya zI@RG(Mv--@PAntH+EwN%(PQnZl`?XyQ*HlY)L5tLwK8I?J*gHV#3qk!Sus{digl{q zEThCax&*I8h;^#=E74({Dz|l!VQqOLD5Jt!T}e|$gte3bClmwj%&MXi3Dz1)nlcKk zB@gmy1Xwvs8U587NQz42SJ_r2>Z>Ksc4fp@OP3U7v{y@>1qPofoDWa>SsYG*?*{VcxmFMtE6j#~7Wdv7C=C=~P)sk&h zBDcz3sfpSu`?HMLYE9(Agu>)wP-;*mQmgFgN|aVz$x@BbDwk7@&MNnT-)Uslmq|zB zzm$PjzM>3q$^SX=fBM}~kMIA7gW+rZpO;9_=Km`K?;P9zb;kpl|9-EF@tlRae;z#({Z&>>6xp^bRs@ATBo_I@R{4+FojEd1}wgE=MapN588YTBs{OuZkWf%X- z&2~*Q`osRu%w)w0t4lm+5B_Bw4!qsu1mhEnyl$3&x4^d<;h}eF?9mRn$jE2tiDVz& zHo(9jl>l5FUb{FTIo+`Al)=&nBQq(Yx3=*U-SOzG?35P~B^f0?&0!Q%|9{)N zmgcl!Abf^jp$^O>nIsr6kJL;LeN2u$w8y5+IA9VF#P(oO!lC``-ESqofE3zh=w-jm zWQ?ViWNEeb+N)10OQX^n_-6dU%xTMJTpcW`Fefg}XiKl!Dq}7-X`?*pi|2jycBE(Y z#nrNF%wB6ndp%dB>3qBBJO>y4d^Y6#80)|*D{|J8OdyP|Kr8gZN0ei$ZkTL6){B%B z<8FDMwk81>j94>@c$L-i z8PVN%%%Ooo)NrcqSs69`zFYI$pxRA9u??bz*oQbe(afPlIUb=nzDgGHWNGPTxR+kp zIx@!JGzya>(5aI~{w#>YxupcI*I?!hRr6}Ka!eR8B;QUZBs~e9K);JiM|&_lh%&%b z!In%*1hpdO0Nlcq*Glk$vHZ{;S&cQFk=0ZB1f={`fGKuq&b6%WQbU;-8|=A!fe!SO z8h9O&0V2z#@#PB*WI1I&{wd$g+6%NhVgWj1q8{SK+MHHXezQP*3wctYWbUtdQ!CYL z_h>k4da=GWMy$D9g#6D@Zzz$}@CWx(;)Ac5`Ua5~CPIic<=Rg*XhiLtGK!6|K`q@K zw%YX9@m|yI&}&_--rQkIO?8RFCuP%A4sO=Tm{Ir>?MZi+|rJU zAJ(>xkhL;JDsuvf)!AE3-kOp|q%x5jzg6)UC!ZGlcRc(2Z^`0cqK*Uozb+}+pgnZ) z{iomSi~n~FJn#QycbI>##4e#lo0urQyx4WntSzMZzt*>OEPbz0|VO z!4xm7;9|lz4t2fcj_hiXaZIS_ckA5qp88{LE6IhidR3>@wsS*y>uLLn#&aCK@W-VW zgl2h(4zd52<90iBDn&JmtK2ScbL~bEV%#j{$upIGGTzrHbMVW{@5Go11OkCTAP@)y f0)apv5C{YUfj}S-2m}IwU>|+~rQ-ge0H6Q>$7DuA literal 0 HcmV?d00001 From 2d099680d01c117fa3d2177e95bb297ff908c1b4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 15:39:59 -0700 Subject: [PATCH 065/331] Rename apache compatibility test tarball --- .../testdata/{configs.tar.gz => apache.tar.gz} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename certbot-compatibility-test/certbot_compatibility_test/testdata/{configs.tar.gz => apache.tar.gz} (100%) diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/apache.tar.gz similarity index 100% rename from certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz rename to certbot-compatibility-test/certbot_compatibility_test/testdata/apache.tar.gz From a76c36bf1273320da724f727a26ebc73a663c8ff Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 15:52:33 -0700 Subject: [PATCH 066/331] Remove old Dockerfiles --- .../configurators/apache/Dockerfile | 20 ------------------- .../configurators/nginx/Dockerfile | 20 ------------------- 2 files changed, 40 deletions(-) delete mode 100644 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/Dockerfile delete mode 100644 certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/Dockerfile diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/Dockerfile b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/Dockerfile deleted file mode 100644 index ea9bb857f..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM httpd -MAINTAINER Brad Warren - -RUN mkdir /var/run/apache2 - -ENV APACHE_RUN_USER=daemon \ - APACHE_RUN_GROUP=daemon \ - APACHE_PID_FILE=/usr/local/apache2/logs/httpd.pid \ - APACHE_RUN_DIR=/var/run/apache2 \ - APACHE_LOCK_DIR=/var/lock \ - APACHE_LOG_DIR=/usr/local/apache2/logs - -COPY certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh /usr/local/bin/ -COPY certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh /usr/local/bin/ -COPY certbot-compatibility-test/certbot_compatibility_test/testdata/rsa1024_key2.pem /usr/local/apache2/conf/ -COPY certbot-compatibility-test/certbot_compatibility_test/testdata/empty_cert.pem /usr/local/apache2/conf/ - -# Note: this only exposes the port to other docker containers. You -# still have to bind to 443@host at runtime. -EXPOSE 443 diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/Dockerfile b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/Dockerfile deleted file mode 100644 index ea9bb857f..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM httpd -MAINTAINER Brad Warren - -RUN mkdir /var/run/apache2 - -ENV APACHE_RUN_USER=daemon \ - APACHE_RUN_GROUP=daemon \ - APACHE_PID_FILE=/usr/local/apache2/logs/httpd.pid \ - APACHE_RUN_DIR=/var/run/apache2 \ - APACHE_LOCK_DIR=/var/lock \ - APACHE_LOG_DIR=/usr/local/apache2/logs - -COPY certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh /usr/local/bin/ -COPY certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh /usr/local/bin/ -COPY certbot-compatibility-test/certbot_compatibility_test/testdata/rsa1024_key2.pem /usr/local/apache2/conf/ -COPY certbot-compatibility-test/certbot_compatibility_test/testdata/empty_cert.pem /usr/local/apache2/conf/ - -# Note: this only exposes the port to other docker containers. You -# still have to bind to 443@host at runtime. -EXPOSE 443 From 0edb1f6792575d303d592acafd9441cf3ed17367 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 16:08:30 -0700 Subject: [PATCH 067/331] Add certbot-compatibility-test Dockerfiles --- certbot-compatibility-test/Dockerfile | 51 ++++++++++++++++++++ certbot-compatibility-test/Dockerfile-apache | 6 +++ certbot-compatibility-test/Dockerfile-nginx | 6 +++ 3 files changed, 63 insertions(+) create mode 100644 certbot-compatibility-test/Dockerfile create mode 100644 certbot-compatibility-test/Dockerfile-apache create mode 100644 certbot-compatibility-test/Dockerfile-nginx diff --git a/certbot-compatibility-test/Dockerfile b/certbot-compatibility-test/Dockerfile new file mode 100644 index 000000000..d5ef9841c --- /dev/null +++ b/certbot-compatibility-test/Dockerfile @@ -0,0 +1,51 @@ +FROM debian:jessie +MAINTAINER Brad Warren + +WORKDIR /opt/certbot + +# no need to mkdir anything: +# https://docs.docker.com/reference/builder/#copy +# If doesn't exist, it is created along with all missing +# directories in its path. + +# TODO: Install non-default Python versions for tox. +# TODO: Install Apache/Nginx for plugin development. +COPY certbot-auto /opt/certbot/src/certbot-auto +RUN /opt/certbot/src/certbot-auto -n --os-packages-only + +# the above is not likely to change, so by putting it further up the +# Dockerfile we make sure we cache as much as possible + +COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/certbot/src/ + +# all above files are necessary for setup.py, however, package source +# code directory has to be copied separately to a subdirectory... +# https://docs.docker.com/reference/builder/#copy: "If is a +# directory, the entire contents of the directory are copied, +# including filesystem metadata. Note: The directory itself is not +# copied, just its contents." Order again matters, three files are far +# more likely to be cached than the whole project directory + +COPY certbot /opt/certbot/src/certbot/ +COPY acme /opt/certbot/src/acme/ +COPY certbot-apache /opt/certbot/src/certbot-apache/ +COPY certbot-nginx /opt/certbot/src/certbot-nginx/ +COPY certbot-compatibility-test /opt/certbot/src/certbot-compatibility-test/ + +RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \ + /opt/certbot/venv/bin/pip install -U setuptools && \ + /opt/certbot/venv/bin/pip install -U pip && \ + /opt/certbot/venv/bin/pip install \ + -e /opt/certbot/src/acme \ + -e /opt/certbot/src \ + -e /opt/certbot/src/certbot-apache \ + -e /opt/certbot/src/certbot-nginx \ + -e /opt/certbot/src/certbot-compatibility-test \ + -e /opt/certbot/src[dev,docs] + +# install in editable mode (-e) to save space: it's not possible to +# "rm -rf /opt/certbot/src" (it's stays in the underlaying image); +# this might also help in debugging: you can "docker run --entrypoint +# bash" and investigate, apply patches, etc. + +ENV PATH /opt/certbot/venv/bin:$PATH diff --git a/certbot-compatibility-test/Dockerfile-apache b/certbot-compatibility-test/Dockerfile-apache new file mode 100644 index 000000000..5c0495966 --- /dev/null +++ b/certbot-compatibility-test/Dockerfile-apache @@ -0,0 +1,6 @@ +FROM certbot-compatibility-test +MAINTAINER Brad Warren + +RUN apt-get install apache2 -y + +ENTRYPOINT [ "certbot-compatibility-test", "-p", "apache" ] diff --git a/certbot-compatibility-test/Dockerfile-nginx b/certbot-compatibility-test/Dockerfile-nginx new file mode 100644 index 000000000..4ade03065 --- /dev/null +++ b/certbot-compatibility-test/Dockerfile-nginx @@ -0,0 +1,6 @@ +FROM certbot-compatibility-test +MAINTAINER Brad Warren + +RUN apt-get install nginx -y + +ENTRYPOINT [ "certbot-compatibility-test", "-p", "nginx" ] From 07b85f9f90a12b3b52c19fb0b379955f728095ca Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 16:32:38 -0700 Subject: [PATCH 068/331] Make testdata the CWD of compatibility test dockerfiles --- certbot-compatibility-test/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-compatibility-test/Dockerfile b/certbot-compatibility-test/Dockerfile index d5ef9841c..e445a3555 100644 --- a/certbot-compatibility-test/Dockerfile +++ b/certbot-compatibility-test/Dockerfile @@ -1,8 +1,6 @@ FROM debian:jessie MAINTAINER Brad Warren -WORKDIR /opt/certbot - # no need to mkdir anything: # https://docs.docker.com/reference/builder/#copy # If doesn't exist, it is created along with all missing @@ -48,4 +46,6 @@ RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \ # this might also help in debugging: you can "docker run --entrypoint # bash" and investigate, apply patches, etc. +WORKDIR /opt/certbot/src/certbot-compatibility-test/certbot_compatibility_test/testdata + ENV PATH /opt/certbot/venv/bin:$PATH From fc86f869a71db80c613e60dbd206e2764908c354 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 16:33:56 -0700 Subject: [PATCH 069/331] add compatibility tests to travis --- tox.ini | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 27979d9df..30c895199 100644 --- a/tox.ini +++ b/tox.ini @@ -79,7 +79,6 @@ commands = pip install -e acme -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot {toxinidir}/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test --debian-modules - [testenv:le_auto] # At the moment, this tests under Python 2.7 only, as only that version is # readily available on the Trusty Docker image. @@ -89,3 +88,21 @@ commands = whitelist_externals = docker passenv = DOCKER_* + +[testenv:apache_compat] +commands = + docker build -t certbot-compatibility-test -f certbot-compatibility-test/Dockerfile . + docker build -t apache-compat -f certbot-compatibility-test/Dockerfile-apache . + docker run --rm -it apache-compat -c apache.tar.gz -vvvv +whitelist_externals = + docker +passenv = DOCKER_* + +[testenv:nginx_compat] +commands = + docker build -t certbot-compatibility-test -f certbot-compatibility-test/Dockerfile . + docker build -t nginx-compat -f certbot-compatibility-test/Dockerfile-nginx . + docker run --rm -it nginx-compat -c nginx.tar.gz -vvvv +whitelist_externals = + docker +passenv = DOCKER_* From f864cd0cfe29a1ec8caa82204fd6e71ca0e366dc Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 16:43:15 -0700 Subject: [PATCH 070/331] Add nginxroundtrip to tox --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index 30c895199..64b5170db 100644 --- a/tox.ini +++ b/tox.ini @@ -79,6 +79,11 @@ commands = pip install -e acme -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot {toxinidir}/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test --debian-modules +[testenv:nginxroundtrip] +commands = + pip install -e acme[dev] -e .[dev] -e certbot-nginx + python certbot-compatibility-test/nginx/roundtrip.py certbot-compatibility-test/nginx/nginx-roundtrip-testdata + [testenv:le_auto] # At the moment, this tests under Python 2.7 only, as only that version is # readily available on the Trusty Docker image. From 5dda27d757b5200651eaac83bc3e62b99b112880 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 16:46:53 -0700 Subject: [PATCH 071/331] Add nginxroundtrip and compatibility-tests to travis --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6f964dbec..5ccf39811 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,8 @@ matrix: - python: "2.7" env: TOXENV=apacheconftest sudo: required + - python: "2.7" + env: TOXENV=nginxroundtrip - python: "2.7" env: TOXENV=py27 BOULDER_INTEGRATION=1 sudo: true @@ -53,6 +55,16 @@ matrix: services: docker before_install: addons: + - sudo: required + env: TOXENV=apache_compat + services: docker + before_install: + addons: + - sudo: required + env: TOXENV=nginx_compat + services: docker + before_install: + addons: - python: "2.7" env: TOXENV=cover - python: "3.3" From cfc8ce9db436d5a0265a9569bf73e1f887126431 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 10 Aug 2016 17:01:34 -0700 Subject: [PATCH 072/331] Add function docstring --- certbot-nginx/certbot_nginx/constants.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/constants.py b/certbot-nginx/certbot_nginx/constants.py index 98f924b2b..8cf1f6bc9 100644 --- a/certbot-nginx/certbot_nginx/constants.py +++ b/certbot-nginx/certbot_nginx/constants.py @@ -21,5 +21,12 @@ def os_constant(key): # XXX TODO: In the future, this could return different constants # based on what OS we are running under. To see an # approach to how to handle different OSes, see the - # apache version of this file. + # apache version of this file. Currently, we do not + # actually have any OS-specific constants on Nginx. + """ + Get a constant value for operating system + + :param key: name of cli constant + :return: value of constant for active os + """ return CLI_DEFAULTS[key] From 4bbb12f182341c4d05fe808b03424467949e13b6 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 10 Aug 2016 17:16:54 -0700 Subject: [PATCH 073/331] Satisfying some lint complaints --- .../configurators/apache/common.py | 1 - .../configurators/nginx/__init__.py | 2 +- .../configurators/nginx/common.py | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 4e612bbd5..b7b1f52c2 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -1,5 +1,4 @@ """Provides a common base for Apache proxies""" -import re import os import shutil import subprocess diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/__init__.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/__init__.py index d559d0645..ed294abe6 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/__init__.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/__init__.py @@ -1 +1 @@ -"""Certbot compatibility test Apache configurators""" +"""Certbot compatibility test Nginx configurators""" diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index 0d72605b7..311ae4ba6 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -1,5 +1,4 @@ -"""Provides a common base for Apache proxies""" -import re +"""Provides a common base for Nginx proxies""" import os import shutil import subprocess @@ -78,7 +77,7 @@ class Proxy(configurators_common.Proxy): # This does not appear to exist in nginx (yet?) # self.le_config.nginx_handle_modules = self.le_config.nginx_handle_mods - conf=configuration.NamespaceConfig(self.le_config) + conf = configuration.NamespaceConfig(self.le_config) zope.component.provideUtility(conf) self._nginx_configurator = configurator.NginxConfigurator( config=conf, name="nginx") @@ -132,6 +131,7 @@ def _get_names(config): for line in open(os.path.join(root, this_file)): if line.strip().startswith("server_name"): names = line.partition("server_name")[2].rpartition(";")[0] - [all_names.add(n) for n in names.split()] + for n in names.split(): + all_names.add(n) non_ip_names = set(n for n in all_names if not util.IP_REGEX.match(n)) return all_names, non_ip_names From 1f471da7685a790d9bc4f0d66b5f359df0877025 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 10 Aug 2016 17:39:29 -0700 Subject: [PATCH 074/331] Remove code duplication to make pylint happy --- .../configurators/apache/common.py | 38 +------------ .../configurators/common.py | 35 ++++++++++++ .../configurators/nginx/common.py | 57 +------------------ 3 files changed, 39 insertions(+), 91 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index b7b1f52c2..64170ca72 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -27,30 +27,18 @@ class Proxy(configurators_common.Proxy): self.le_config.apache_le_vhost_ext = "-le-ssl.conf" self.modules = self.server_root = self.test_conf = self.version = None - self._apache_configurator = self._all_names = self._test_names = None patch = mock.patch( "certbot_apache.configurator.display_ops.select_vhost") mock_display = patch.start() mock_display.side_effect = le_errors.PluginError( "Unable to determine vhost") - def __getattr__(self, name): - """Wraps the Apache Configurator methods""" - method = getattr(self._apache_configurator, name, None) - if callable(method): - return method - else: - raise AttributeError() - def load_config(self): """Loads the next configuration for the plugin to test""" - config = super(Proxy, self).load_config() self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) - # with open(os.path.join(config, "config_file")) as f: - # config_file = os.path.join(server_root, f.readline().rstrip()) shutil.rmtree("/etc/apache2") shutil.copytree(server_root, "/etc/apache2", symlinks=True) @@ -73,38 +61,16 @@ class Proxy(configurators_common.Proxy): # An alias self.le_config.apache_handle_modules = self.le_config.apache_handle_mods - self._apache_configurator = configurator.ApacheConfigurator( + self._configurator = configurator.ApacheConfigurator( config=configuration.NamespaceConfig(self.le_config), name="apache") - self._apache_configurator.prepare() + self._configurator.prepare() def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" super(Proxy, self).cleanup_from_tests() mock.patch.stopall() - def get_all_names_answer(self): - """Returns the set of domain names that the plugin should find""" - if self._all_names: - return self._all_names - else: - raise errors.Error("No configuration file loaded") - - def get_testable_domain_names(self): - """Returns the set of domain names that can be tested against""" - if self._test_names: - return self._test_names - else: - return {"example.com"} - - def deploy_cert(self, domain, cert_path, key_path, chain_path=None, - fullchain_path=None): - """Installs cert""" - cert_path, key_path, chain_path = self.copy_certs_and_keys( - cert_path, key_path, chain_path) - self._apache_configurator.deploy_cert( - domain, cert_path, key_path, chain_path, fullchain_path) - def _get_server_root(config): """Returns the server root directory in config""" diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py index 03128cc86..2a800c1c2 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py @@ -5,6 +5,7 @@ import shutil import tempfile from certbot import constants +from certbot_compatibility_test import errors from certbot_compatibility_test import util @@ -31,6 +32,18 @@ class Proxy(object): self.args = args self.http_port = 80 self.https_port = 443 + self._configurator = self._all_names = self._test_names = None + + def __getattr__(self, name): + """Wraps the configurator methods""" + if self._configurator is None: + raise AttributeError() + + method = getattr(self._configurator, name, None) + if callable(method): + return method + else: + raise AttributeError() def has_more_configs(self): """Returns true if there are more configs to test""" @@ -63,3 +76,25 @@ class Proxy(object): chain = None return cert, key, chain + + def get_all_names_answer(self): + """Returns the set of domain names that the plugin should find""" + if self._all_names: + return self._all_names + else: + raise errors.Error("No configuration file loaded") + + def get_testable_domain_names(self): + """Returns the set of domain names that can be tested against""" + if self._test_names: + return self._test_names + else: + return {"example.com"} + + def deploy_cert(self, domain, cert_path, key_path, chain_path=None, + fullchain_path=None): + """Installs cert""" + cert_path, key_path, chain_path = self.copy_certs_and_keys( + cert_path, key_path, chain_path) + self._configurator.deploy_cert( + domain, cert_path, key_path, chain_path, fullchain_path) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index 311ae4ba6..3622bee41 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -3,11 +3,9 @@ import os import shutil import subprocess -import mock import zope.interface from certbot import configuration -from certbot import errors as le_errors from certbot_nginx import configurator from certbot_nginx import constants from certbot_compatibility_test import errors @@ -22,36 +20,15 @@ class Proxy(configurators_common.Proxy): """A common base for Nginx test configurators""" def __init__(self, args): - # XXX: This is still apache-specific """Initializes the plugin with the given command line args""" super(Proxy, self).__init__(args) - self.le_config.apache_le_vhost_ext = "-le-ssl.conf" - - self.modules = self.server_root = self.test_conf = self.version = None - self._nginx_configurator = self._all_names = self._test_names = None - patch = mock.patch( - "certbot_apache.configurator.display_ops.select_vhost") - mock_display = patch.start() - mock_display.side_effect = le_errors.PluginError( - "Unable to determine vhost") - - def __getattr__(self, name): - """Wraps the Nginx Configurator methods""" - method = getattr(self._nginx_configurator, name, None) - if callable(method): - return method - else: - raise AttributeError() def load_config(self): """Loads the next configuration for the plugin to test""" - config = super(Proxy, self).load_config() self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) - # with open(os.path.join(config, "config_file")) as f: - # config_file = os.path.join(server_root, f.readline().rstrip()) # XXX: Deleting all of this is kind of scary unless the test # instances really each have a complete configuration! @@ -74,41 +51,11 @@ class Proxy(configurators_common.Proxy): for k in constants.CLI_DEFAULTS.keys(): setattr(self.le_config, "nginx_" + k, constants.os_constant(k)) - # This does not appear to exist in nginx (yet?) - # self.le_config.nginx_handle_modules = self.le_config.nginx_handle_mods - conf = configuration.NamespaceConfig(self.le_config) zope.component.provideUtility(conf) - self._nginx_configurator = configurator.NginxConfigurator( + self._configurator = configurator.NginxConfigurator( config=conf, name="nginx") - self._nginx_configurator.prepare() - - def cleanup_from_tests(self): - """Performs any necessary cleanup from running plugin tests""" - super(Proxy, self).cleanup_from_tests() - mock.patch.stopall() - - def get_all_names_answer(self): - """Returns the set of domain names that the plugin should find""" - if self._all_names: - return self._all_names - else: - raise errors.Error("No configuration file loaded") - - def get_testable_domain_names(self): - """Returns the set of domain names that can be tested against""" - if self._test_names: - return self._test_names - else: - return {"example.com"} - - def deploy_cert(self, domain, cert_path, key_path, chain_path=None, - fullchain_path=None): - """Installs cert""" - cert_path, key_path, chain_path = self.copy_certs_and_keys( - cert_path, key_path, chain_path) - self._nginx_configurator.deploy_cert( - domain, cert_path, key_path, chain_path, fullchain_path) + self._configurator.prepare() def _get_server_root(config): From d541adcfa8d5ee5d867f260f4b465e8753709c8c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 12 Aug 2016 16:20:46 -0700 Subject: [PATCH 075/331] fix extra or lack of spacing between words in help for renew flags --- certbot/cli.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index b01b0a7f1..456a8d7f3 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -818,35 +818,39 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " used to create obtain or most recently successfully renew each" " certificate lineage. You can try it with `--dry-run` first. For" " more fine-grained control, you can renew individual lineages with" - " the `certonly` subcommand. Hooks are available to run commands " + " the `certonly` subcommand. Hooks are available to run commands" " before and after renewal; see" - " https://certbot.eff.org/docs/using.html#renewal for more information on these.") + " https://certbot.eff.org/docs/using.html#renewal for more" + " information on these.") helpful.add( "renew", "--pre-hook", - help="Command to be run in a shell before obtaining any certificates. Intended" - " primarily for renewal, where it can be used to temporarily shut down a" - " webserver that might conflict with the standalone plugin. This will " - " only be called if a certificate is actually to be obtained/renewed. ") + help="Command to be run in a shell before obtaining any certificates." + " Intended primarily for renewal, where it can be used to temporarily" + " shut down a webserver that might conflict with the standalone" + " plugin. This will only be called if a certificate is actually to be" + " obtained/renewed.") helpful.add( "renew", "--post-hook", - help="Command to be run in a shell after attempting to obtain/renew " - " certificates. Can be used to deploy renewed certificates, or to restart" - " any servers that were stopped by --pre-hook. This is only run if" - " an attempt was made to obtain/renew a certificate.") + help="Command to be run in a shell after attempting to obtain/renew" + " certificates. Can be used to deploy renewed certificates, or to" + " restart any servers that were stopped by --pre-hook. This is only" + " run if an attempt was made to obtain/renew a certificate.") helpful.add( "renew", "--renew-hook", - help="Command to be run in a shell once for each successfully renewed certificate." - "For this command, the shell variable $RENEWED_LINEAGE will point to the" - "config live subdirectory containing the new certs and keys; the shell variable " - "$RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains") + help="Command to be run in a shell once for each successfully renewed" + " certificate. For this command, the shell variable $RENEWED_LINEAGE" + " will point to the config live subdirectory containing the new certs" + " and keys; the shell variable $RENEWED_DOMAINS will contain a" + " space-delimited list of renewed cert domains") helpful.add( "renew", "--disable-hook-validation", action='store_false', dest='validate_hooks', default=True, - help="Ordinarily the commands specified for --pre-hook/--post-hook/--renew-hook" - " will be checked for validity, to see if the programs being run are in the $PATH," - " so that mistakes can be caught early, even when the hooks aren't being run just yet." - " The validation is rather simplistic and fails if you use more advanced" + help="Ordinarily the commands specified for" + " --pre-hook/--post-hook/--renew-hook will be checked for validity, to" + " see if the programs being run are in the $PATH, so that mistakes can" + " be caught early, even when the hooks aren't being run just yet. The" + " validation is rather simplistic and fails if you use more advanced" " shell constructs, so you can use this switch to disable it.") helpful.add_deprecated_argument("--agree-dev-preview", 0) From 3a9769acbfcce37bbda5fbdf31b89729567ba73a Mon Sep 17 00:00:00 2001 From: Jon Walsh Date: Mon, 8 Aug 2016 15:13:23 +0700 Subject: [PATCH 076/331] Replace '-' with '_' before filtering plugin settings This bug notably occurs when renewing certs for with the plugin `letsencrypt-s3front` --- certbot/renewal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/renewal.py b/certbot/renewal.py index d0c797a08..339d7b7ff 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -143,6 +143,7 @@ def _restore_plugin_configs(config, renewalparams): if renewalparams.get("installer", None) is not None: plugin_prefixes.append(renewalparams["installer"]) for plugin_prefix in set(plugin_prefixes): + plugin_prefix = plugin_prefix.replace('-', '_') for config_item, config_value in six.iteritems(renewalparams): if config_item.startswith(plugin_prefix + "_") and not cli.set_by_cli(config_item): # Values None, True, and False need to be treated specially, From f7a09e5e44b25d5631898f771022ac94df4334ae Mon Sep 17 00:00:00 2001 From: Mathieu Leduc-Hamel Date: Sun, 7 Aug 2016 10:51:25 -0400 Subject: [PATCH 077/331] Add dns-01 challenge support to the Manual plugin --- certbot/plugins/manual.py | 126 ++++++++++++++++++++--------- certbot/plugins/manual_test.py | 32 +++++--- certbot/plugins/standalone.py | 38 ++------- certbot/plugins/standalone_test.py | 28 ------- certbot/plugins/util.py | 36 +++++++++ certbot/plugins/util_test.py | 38 +++++++++ certbot/tests/acme_util.py | 8 +- certbot/tests/auth_handler_test.py | 8 +- certbot/tests/errors_test.py | 4 +- certbot/util.py | 21 +++++ 10 files changed, 221 insertions(+), 118 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 6c7b822ab..608484031 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -4,26 +4,30 @@ import logging import pipes import shutil import signal -import socket import subprocess import sys import tempfile -import time import six import zope.component import zope.interface +from functools import partial + from acme import challenges from certbot import errors from certbot import interfaces -from certbot.plugins import common +from certbot.plugins import common, util +from certbot.util import busy_wait logger = logging.getLogger(__name__) +SUPPORTED_CHALLENGES = [challenges.HTTP01, challenges.DNS01] + + @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): @@ -41,7 +45,16 @@ class Authenticator(common.Plugin): description = "Manually configure an HTTP server" - MESSAGE_TEMPLATE = """\ + MESSAGE_TEMPLATE = { + "dns-01": """\ +Make sure your dns configuration content the following key before continuing: + +{validation} + +if you didn't, make sure to add the validation as TXT record into your domain +configuration. +""", + "http-01": """\ Make sure your web server displays the following content at {uri} before continuing: @@ -51,7 +64,7 @@ If you don't have HTTP server configured, you can run the following command on the target server (as root): {command} -""" +"""} # a disclaimer about your current IP being transmitted to Let's Encrypt's servers. IP_DISCLAIMER = """\ @@ -86,10 +99,23 @@ s.serve_forever()" """ @classmethod def add_parser_arguments(cls, add): + validator = partial(util.supported_challenges_validator, + supported=SUPPORTED_CHALLENGES) + add("test-mode", action="store_true", help="Test mode. Executes the manual command in subprocess.") add("public-ip-logging-ok", action="store_true", help="Automatically allows public IP logging.") + add("supported-challenges", + help="Supported challenges. Preferred in the order they are listed.", + type=validator, + default="http-01") + + @property + def supported_challenges(self): + """Challenges supported by this plugin.""" + return [challenges.Challenge.TYPES[name] for name in + self.conf("supported-challenges").split(",")] def prepare(self): # pylint: disable=missing-docstring,no-self-use if self.config.noninteractive_mode and not self.conf("test-mode"): @@ -97,38 +123,35 @@ s.serve_forever()" """ def more_info(self): # pylint: disable=missing-docstring,no-self-use return ("This plugin requires user's manual intervention in setting " - "up an HTTP server for solving http-01 challenges and thus " + "up an HTTP server when solving http-01 challenges and thus " "does not need to be run as a privileged process. " "Alternatively shows instructions on how to use Python's " - "built-in HTTP server.") + "built-in HTTP server." + "When solving dns-01 challenges, it simply needs to wait for " + "the proper configuration of the domain's dns") def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.HTTP01] + return self.supported_challenges - def perform(self, achalls): # pylint: disable=missing-docstring + def perform(self, achalls): + # pylint: disable=missing-docstring + mapping = {"http-01": self._perform_http01_challenge, + "dns-01": self._perform_dns01_challenge} responses = [] # TODO: group achalls by the same socket.gethostbyname(_ex) # and prompt only once per server (one "echo -n" per domain) for achall in achalls: - responses.append(self._perform_single(achall)) + responses.append(mapping[achall.typ](achall)) return responses - @classmethod - def _test_mode_busy_wait(cls, port): - while True: - time.sleep(1) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect(("localhost", port)) - except socket.error: # pragma: no cover - pass - else: - break - finally: - sock.close() + def cleanup(self, achalls): + # pylint: disable=missing-docstring + for achall in achalls: + if isinstance(achall.chall, challenges.HTTP01): + self._cleanup_http01_challenge(achall) - def _perform_single(self, achall): + def _perform_http01_challenge(self, achall): # same path for each challenge response would be easier for # users, but will not work if multiple domains point at the # same server: default command doesn't support virtual hosts @@ -161,7 +184,8 @@ s.serve_forever()" """ logger.debug("Manual command running as PID %s.", self._httpd.pid) # give it some time to bootstrap, before we try to verify # (cert generation in case of simpleHttpS might take time) - self._test_mode_busy_wait(port) + busy_wait(port) + if self._httpd.poll() is not None: raise errors.Error("Couldn't execute manual command") else: @@ -171,10 +195,14 @@ s.serve_forever()" """ cli_flag="--manual-public-ip-logging-ok"): raise errors.PluginError("Must agree to IP logging to proceed") - self._notify_and_wait(self.MESSAGE_TEMPLATE.format( - validation=validation, response=response, + message = self._get_message(achall) + + self._notify_and_wait(message.format( + validation=validation, + response=response, uri=achall.chall.uri(achall.domain), - command=command)) + command=command + )) if not response.simple_verify( achall.chall, achall.domain, @@ -183,15 +211,30 @@ s.serve_forever()" """ return response - def _notify_and_wait(self, message): # pylint: disable=no-self-use - # TODO: IDisplay wraps messages, breaking the command - #answer = zope.component.getUtility(interfaces.IDisplay).notification( - # message=message, height=25, pause=True) - sys.stdout.write(message) - six.moves.input("Press ENTER to continue") + def _perform_dns01_challenge(self, achall): + response, validation = achall.response_and_validation() + if not self.conf("test-mode"): + if not self.conf("public-ip-logging-ok"): + if not zope.component.getUtility(interfaces.IDisplay).yesno( + self.IP_DISCLAIMER, "Yes", "No", + cli_flag="--manual-public-ip-logging-ok"): + raise errors.PluginError("Must agree to IP logging to proceed") - def cleanup(self, achalls): - # pylint: disable=missing-docstring,no-self-use,unused-argument + message = self._get_message(achall) + formated_message = message.format(validation=validation, + response=response) + + self._notify_and_wait(formated_message) + + if not response.simple_verify( + achall.chall, achall.domain, + achall.account_key.public_key()): + logger.warning("Self-verify of challenge failed.") + + return response + + def _cleanup_http01_challenge(self, achall): + # pylint: disable=missing-docstring,unused-argument if self.conf("test-mode"): assert self._httpd is not None, ( "cleanup() must be called after perform()") @@ -202,3 +245,14 @@ s.serve_forever()" """ logger.debug("Manual command process already terminated " "with %s code", self._httpd.returncode) shutil.rmtree(self._root) + + def _notify_and_wait(self, message): # pylint: disable=no-self-use + # TODO: IDisplay wraps messages, breaking the command + #answer = zope.component.getUtility(interfaces.IDisplay).notification( + # message=message, height=25, pause=True) + sys.stdout.write(message) + six.moves.input("Press ENTER to continue") + + def _get_message(self, achall): + # pylint: disable=missing-docstring,no-self-use,unused-argument + return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "") diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index dd0905049..0686a24d7 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -24,10 +24,16 @@ class AuthenticatorTest(unittest.TestCase): from certbot.plugins.manual import Authenticator self.config = mock.MagicMock( http01_port=8080, manual_test_mode=False, - manual_public_ip_logging_ok=False, noninteractive_mode=True) + manual_public_ip_logging_ok=False, noninteractive_mode=True, + standalone_supported_challenges="dns-01,http-01") self.auth = Authenticator(config=self.config, name="manual") - self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)] + + self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY) + self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY) + + self.achalls = [self.http01, self.dns01] config_test_mode = mock.MagicMock( http01_port=8080, manual_test_mode=True, noninteractive_mode=True) @@ -56,19 +62,21 @@ class AuthenticatorTest(unittest.TestCase): mock_verify.return_value = True mock_interaction().yesno.return_value = True - resp = self.achalls[0].response(KEY) - self.assertEqual([resp], self.auth.perform(self.achalls)) - self.assertEqual(1, mock_raw_input.call_count) + resp_http = self.http01.response(KEY) + resp_dns = self.dns01.response(KEY) + + self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls)) + self.assertEqual(2, mock_raw_input.call_count) mock_verify.assert_called_with( - self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 8080) + self.http01.challb.chall, "foo.com", KEY.public_key(), 8080) message = mock_stdout.write.mock_calls[0][1][0] - self.assertTrue(self.achalls[0].chall.encode("token") in message) + self.assertTrue(self.http01.chall.encode("token") in message) mock_verify.return_value = False with mock.patch("certbot.plugins.manual.logger") as mock_logger: self.auth.perform(self.achalls) - mock_logger.warning.assert_called_once_with(mock.ANY) + self.assertEqual(2, mock_logger.warning.call_count) @mock.patch("certbot.plugins.manual.zope.component.getUtility") @mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait") @@ -82,10 +90,10 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_command_oserror(self, mock_popen): mock_popen.side_effect = OSError - self.assertEqual([False], self.auth_test_mode.perform(self.achalls)) + self.assertEqual([False], self.auth_test_mode.perform([self.http01])) - @mock.patch("certbot.plugins.manual.socket.socket") - @mock.patch("certbot.plugins.manual.time.sleep", autospec=True) + @mock.patch("certbot.util.socket.socket") + @mock.patch("certbot.util.time.sleep", autospec=True) @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_command_run_failure( self, mock_popen, unused_mock_sleep, unused_mock_socket): diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 97aca351a..bf38ced27 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -1,5 +1,4 @@ """Standalone Authenticator.""" -import argparse import collections import logging import socket @@ -9,6 +8,8 @@ import OpenSSL import six import zope.interface +from functools import partial + from acme import challenges from acme import standalone as acme_standalone @@ -113,36 +114,6 @@ class ServerManager(object): SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01] -def supported_challenges_validator(data): - """Supported challenges validator for the `argparse`. - - It should be passed as `type` argument to `add_argument`. - - """ - challs = data.split(",") - - # tls-sni-01 was dvsni during private beta - if "dvsni" in challs: - logger.info("Updating legacy standalone_supported_challenges value") - challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall - for chall in challs] - data = ",".join(challs) - - unrecognized = [name for name in challs - if name not in challenges.Challenge.TYPES] - if unrecognized: - raise argparse.ArgumentTypeError( - "Unrecognized challenges: {0}".format(", ".join(unrecognized))) - - choices = set(chall.typ for chall in SUPPORTED_CHALLENGES) - if not set(challs).issubset(choices): - raise argparse.ArgumentTypeError( - "Plugin does not support the following (valid) " - "challenges: {0}".format(", ".join(set(challs) - choices))) - - return data - - @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): @@ -176,9 +147,12 @@ class Authenticator(common.Plugin): @classmethod def add_parser_arguments(cls, add): + validator = partial(util.supported_challenges_validator, + supported=SUPPORTED_CHALLENGES) + add("supported-challenges", help="Supported challenges. Preferred in the order they are listed.", - type=supported_challenges_validator, + type=validator, default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES)) @property diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index eb6631732..914332307 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -1,5 +1,4 @@ """Tests for certbot.plugins.standalone.""" -import argparse import socket import unittest @@ -64,33 +63,6 @@ class ServerManagerTest(unittest.TestCase): self.assertEqual(self.mgr.running(), {}) -class SupportedChallengesValidatorTest(unittest.TestCase): - """Tests for plugins.standalone.supported_challenges_validator.""" - - def _call(self, data): - from certbot.plugins.standalone import ( - supported_challenges_validator) - return supported_challenges_validator(data) - - def test_correct(self): - self.assertEqual("tls-sni-01", self._call("tls-sni-01")) - self.assertEqual("http-01", self._call("http-01")) - self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) - self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) - - def test_unrecognized(self): - assert "foo" not in challenges.Challenge.TYPES - self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") - - def test_not_subset(self): - self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") - - def test_dvsni(self): - self.assertEqual("tls-sni-01", self._call("dvsni")) - self.assertEqual("http-01,tls-sni-01", self._call("http-01,dvsni")) - self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) - - class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.standalone.Authenticator.""" diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index b97ca1afd..5a8789c79 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -1,10 +1,13 @@ """Plugin utilities.""" +import argparse import logging import os import socket import zope.component +from acme import challenges + from certbot import interfaces from certbot import util @@ -154,3 +157,36 @@ def already_listening_psutil(port, renewer=False): # name (AccessDenied). pass return False + + +def supported_challenges_validator(data, supported=None): + """Supported challenges validator for the `argparse`. + + It should be passed as `type` argument to `add_argument`. + + :param str data: input value representing the supported_challenges + :returns: original value if valid + """ + challs = data.split(",") + supported = supported or [] + + # tls-sni-01 was dvsni during private beta + if "dvsni" in challs: + logger.info("Updating legacy standalone_supported_challenges value") + challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall + for chall in challs] + data = ",".join(challs) + + unrecognized = [name for name in challs + if name not in challenges.Challenge.TYPES] + if unrecognized: + raise argparse.ArgumentTypeError( + "Unrecognized challenges: {0}".format(", ".join(unrecognized))) + + choices = set(chall.typ for chall in supported) + if not set(challs).issubset(choices): + raise argparse.ArgumentTypeError( + "Plugin does not support the following (valid) " + "challenges: {0}".format(", ".join(set(challs) - choices))) + + return data diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index e1a064fb3..798d7780e 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -1,4 +1,5 @@ """Tests for certbot.plugins.util.""" +import argparse import os import unittest import sys @@ -181,5 +182,42 @@ class AlreadyListeningTestPsutil(unittest.TestCase): mock_net.side_effect = psutil.AccessDenied("") self.assertFalse(self._call(12345)) + +class SupportedChallengesValidatorTest(unittest.TestCase): + """Tests for plugins.standalone.supported_challenges_validator.""" + + def _call(self, data): + from certbot.plugins.util import supported_challenges_validator + from acme import challenges + + supported = [challenges.HTTP01, challenges.DNS01, challenges.TLSSNI01] + + return supported_challenges_validator(data, supported=supported) + + def test_correct(self): + self.assertEqual("tls-sni-01", self._call("tls-sni-01")) + self.assertEqual("http-01", self._call("http-01")) + self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) + + def test_unrecognized(self): + from acme import challenges + + assert "foo" not in challenges.Challenge.TYPES + self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") + + def test_not_subset(self): + self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") + + def test_dvsni(self): + self.assertEqual("tls-sni-01", self._call("dvsni")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,dvsni")) + self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) + + def test_dns01(self): + self.assertEqual("dns-01", self._call("dns-01")) + self.assertEqual("http-01,dns-01", self._call("http-01,dns-01")) + self.assertEqual("dns-01,http-01", self._call("dns-01,http-01")) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/acme_util.py b/certbot/tests/acme_util.py index 4f6e86cc7..de64dfef9 100644 --- a/certbot/tests/acme_util.py +++ b/certbot/tests/acme_util.py @@ -17,9 +17,9 @@ HTTP01 = challenges.HTTP01( token=b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") TLSSNI01 = challenges.TLSSNI01( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) -DNS = challenges.DNS(token=b"17817c66b60ce2e4012dfad92657527a") +DNS01 = challenges.DNS01(token=b"17817c66b60ce2e4012dfad92657527a") -CHALLENGES = [HTTP01, TLSSNI01, DNS] +CHALLENGES = [HTTP01, TLSSNI01, DNS01] def gen_combos(challbs): @@ -45,9 +45,9 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) -DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) +DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING) -CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P] +CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P] def gen_authzr(authz_status, domain, challs, statuses, combos=True): diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index fce130f7c..fcc22f41c 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -111,7 +111,7 @@ class GetAuthorizationsTest(unittest.TestCase): mock_poll.side_effect = self._validate_all self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) - self.mock_auth.get_chall_pref.return_value.append(challenges.DNS) + self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01) authzr = self.handler.get_authorizations(["0"]) @@ -125,7 +125,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertEqual(self.mock_auth.cleanup.call_count, 1) # Test if list first element is TLSSNI01, use typ because it is an achall for achall in self.mock_auth.cleanup.call_args[0][0]: - self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns"]) + self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns-01"]) # Length of authorizations list self.assertEqual(len(authzr), 1) @@ -240,7 +240,7 @@ class PollChallengesTest(unittest.TestCase): from certbot.auth_handler import challb_to_achall self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid self.chall_update[self.doms[0]].append( - challb_to_achall(acme_util.DNS_P, "key", self.doms[0])) + challb_to_achall(acme_util.DNS01_P, "key", self.doms[0])) self.assertRaises( errors.AuthorizationError, self.handler._poll_challenges, self.chall_update, False) @@ -342,7 +342,7 @@ class GenChallengePathTest(unittest.TestCase): self.assertTrue(self._call(challbs[::-1], prefs, None)) def test_not_supported(self): - challbs = (acme_util.DNS_P, acme_util.TLSSNI01_P) + challbs = (acme_util.DNS01_P, acme_util.TLSSNI01_P) prefs = [challenges.TLSSNI01] combos = ((0, 1),) diff --git a/certbot/tests/errors_test.py b/certbot/tests/errors_test.py index 67611ed45..f35a5ea08 100644 --- a/certbot/tests/errors_test.py +++ b/certbot/tests/errors_test.py @@ -16,12 +16,12 @@ class FaiiledChallengesTest(unittest.TestCase): from certbot.errors import FailedChallenges self.error = FailedChallenges(set([achallenges.DNS( domain="example.com", challb=messages.ChallengeBody( - chall=acme_util.DNS, uri=None, + chall=acme_util.DNS01, uri=None, error=messages.Error(typ="tls", detail="detail")))])) def test_str(self): self.assertTrue(str(self.error).startswith( - "Failed authorization procedure. example.com (dns): tls")) + "Failed authorization procedure. example.com (dns-01): tls")) class StandaloneBindErrorTest(unittest.TestCase): diff --git a/certbot/util.py b/certbot/util.py index e78ae664c..e5d671ce1 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -14,6 +14,7 @@ import socket import stat import subprocess import sys +import time import configargparse @@ -474,3 +475,23 @@ def get_strict_version(normalized): # strict version ending with "a" and a number designates a pre-release # pylint: disable=no-member return distutils.version.StrictVersion(normalized.replace(".dev", "a")) + + +def busy_wait(port, host="localhost"): + """Artificialy wait a fixed amount of time on a specific host and port + + :param str port: port of the connection + :param str host: hostname of the connection, "localhost" if None + + """ + while True: + time.sleep(1) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((host, port)) + except socket.error: # pragma: no cover + pass + else: + break + finally: + sock.close() From fff459e6f53a7a03122fd0af699e2a8e5b1c6aec Mon Sep 17 00:00:00 2001 From: Mathieu Leduc-Hamel Date: Sat, 13 Aug 2016 11:09:17 -0400 Subject: [PATCH 078/331] Factored out the ip logging permission functionalities --- certbot/plugins/manual.py | 48 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 608484031..b3cb2bec3 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -47,12 +47,11 @@ class Authenticator(common.Plugin): MESSAGE_TEMPLATE = { "dns-01": """\ -Make sure your dns configuration content the following key before continuing: +To prove control of the domain {domain}, please deploy a DNS TXT record with the following value: {validation} -if you didn't, make sure to add the validation as TXT record into your domain -configuration. +Once this is deployed, """, "http-01": """\ Make sure your web server displays the following content at @@ -189,20 +188,14 @@ s.serve_forever()" """ if self._httpd.poll() is not None: raise errors.Error("Couldn't execute manual command") else: - if not self.conf("public-ip-logging-ok"): - if not zope.component.getUtility(interfaces.IDisplay).yesno( - self.IP_DISCLAIMER, "Yes", "No", - cli_flag="--manual-public-ip-logging-ok"): - raise errors.PluginError("Must agree to IP logging to proceed") - message = self._get_message(achall) + uri = achall.chall.uri(achall.domain) + formated_message = message.format(validation=validation, + response=response, + uri=uri, + command=command) - self._notify_and_wait(message.format( - validation=validation, - response=response, - uri=achall.chall.uri(achall.domain), - command=command - )) + self._ip_logging_permission(response, formated_message) if not response.simple_verify( achall.chall, achall.domain, @@ -214,17 +207,11 @@ s.serve_forever()" """ def _perform_dns01_challenge(self, achall): response, validation = achall.response_and_validation() if not self.conf("test-mode"): - if not self.conf("public-ip-logging-ok"): - if not zope.component.getUtility(interfaces.IDisplay).yesno( - self.IP_DISCLAIMER, "Yes", "No", - cli_flag="--manual-public-ip-logging-ok"): - raise errors.PluginError("Must agree to IP logging to proceed") - message = self._get_message(achall) formated_message = message.format(validation=validation, + domain=achall.domain, response=response) - - self._notify_and_wait(formated_message) + self._ip_logging_permission(response, formated_message) if not response.simple_verify( achall.chall, achall.domain, @@ -246,13 +233,26 @@ s.serve_forever()" """ "with %s code", self._httpd.returncode) shutil.rmtree(self._root) - def _notify_and_wait(self, message): # pylint: disable=no-self-use + def _notify_and_wait(self, message): + # pylint: disable=no-self-use # TODO: IDisplay wraps messages, breaking the command #answer = zope.component.getUtility(interfaces.IDisplay).notification( # message=message, height=25, pause=True) sys.stdout.write(message) six.moves.input("Press ENTER to continue") + def _ip_logging_permission(self, response, formated_message): + # pylint: disable=missing-docstring + if not self.conf("public-ip-logging-ok"): + if not zope.component.getUtility(interfaces.IDisplay).yesno( + self.IP_DISCLAIMER, "Yes", "No", + cli_flag="--manual-public-ip-logging-ok"): + raise errors.PluginError("Must agree to IP logging to proceed") + else: + self.config.namespace.manual_public_ip_logging_ok = True + + self._notify_and_wait(formated_message) + def _get_message(self, achall): # pylint: disable=missing-docstring,no-self-use,unused-argument return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "") From 4a28bb1af7dfa113578c4081f91914d9e420ee93 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 16 Aug 2016 12:37:45 -0700 Subject: [PATCH 079/331] clarify invalid email error in non-interactive --- certbot/display/ops.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 4db6d71e2..901e7cd04 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -48,7 +48,10 @@ def get_email(invalid=False, optional=True): invalid_prefix + msg if invalid else msg) except errors.MissingCommandlineFlag: msg = ("You should register before running non-interactively, " - "or provide --agree-tos and --email flags") + "or provide --agree-tos and --email flags. " + "If you have specified an email with the --email flag, " + "please make sure that you entered it correctly and the " + "domain is valid.") raise errors.MissingCommandlineFlag(msg) if code != display_util.OK: From 0b0eca323cc92a42c492336921a22d7a9ac883d3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 15:36:41 -0700 Subject: [PATCH 080/331] Remove extra newline --- certbot-nginx/certbot_nginx/nginxparser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 7e2cd533a..6f2a3ec70 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -292,7 +292,6 @@ class UnspacedList(list): # 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 From 7fb5cf1cf54c5c5ab10f368d0798ca0fa5164792 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 15:46:31 -0700 Subject: [PATCH 081/331] Catch all pyparsing exceptions --- certbot-nginx/certbot_nginx/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 73224ea1f..229b720b6 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -169,7 +169,7 @@ class NginxParser(object): trees.append(parsed) except IOError: logger.warning("Could not open file: %s", item) - except pyparsing.ParseException: + except pyparsing.ParseBaseException: logger.debug("Could not parse file: %s", item) return trees From 9ecca9bdf01ad7eccc40c744bc399b63eefc16cb Mon Sep 17 00:00:00 2001 From: Benjamin Piouffle Date: Wed, 17 Aug 2016 01:12:15 +0200 Subject: [PATCH 082/331] Update README.rst Link to https://certbot.eff.org/ was broken --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e079cb36d..0b8e652c0 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ If you'd like to contribute to this project please read `Developer Guide Installation ------------ -The easiest way to install Certbot is by visiting certbot.eff.org_, where you can +The easiest way to install Certbot is by visiting https://certbot.eff.org, where you can find the correct installation instructions for many web server and OS combinations. For more information, see the `User Guide `_. From 7767975204e82c4764964dd5a84af448b4eaed08 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 16 Aug 2016 16:13:31 -0700 Subject: [PATCH 083/331] move error outside of get_email --- certbot/client.py | 10 ++++++++-- certbot/display/ops.py | 5 +---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 119fb0947..cb8fc623c 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -150,8 +150,14 @@ def perform_registration(acme, config): return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error as e: if e.typ == "urn:acme:error:invalidEmail": - config.namespace.email = display_ops.get_email(invalid=True) - return perform_registration(acme, config) + if config.noninteractive_mode: + msg = ("The email you specified was unable to be verified " + "by acme. Please ensure it is a valid email and " + "attempt registration again.") + raise erros.MissingCommandlineFlag(msg) + else: + config.namespace.email = display_ops.get_email(invalid=True) + return perform_registration(acme, config) else: raise diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 901e7cd04..e8520fe96 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -48,10 +48,7 @@ def get_email(invalid=False, optional=True): invalid_prefix + msg if invalid else msg) except errors.MissingCommandlineFlag: msg = ("You should register before running non-interactively, " - "or provide --agree-tos and --email flags. " - "If you have specified an email with the --email flag, " - "please make sure that you entered it correctly and the " - "domain is valid.") + "or provide --agree-tos and --email flags.") raise errors.MissingCommandlineFlag(msg) if code != display_util.OK: From 6e550f70b0eb6f5661b740b871dd6a6e9c7330d8 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 16 Aug 2016 16:17:51 -0700 Subject: [PATCH 084/331] fix typo --- certbot/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index cb8fc623c..03525cc0d 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -154,7 +154,7 @@ def perform_registration(acme, config): msg = ("The email you specified was unable to be verified " "by acme. Please ensure it is a valid email and " "attempt registration again.") - raise erros.MissingCommandlineFlag(msg) + raise errors.MissingCommandlineFlag(msg) else: config.namespace.email = display_ops.get_email(invalid=True) return perform_registration(acme, config) From ae23800e538473e9c335d92d15991f13c5f32641 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 16:37:40 -0700 Subject: [PATCH 085/331] Comment code that confused bmw --- certbot-nginx/certbot_nginx/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 229b720b6..703a33cf1 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -329,6 +329,7 @@ class NginxParser(object): tup = [None, None, vhost.filep] if vhost.ssl: for directive in vhost.raw: + # A directive can be an empty list to preserve whitespace if not directive: continue if directive[0] == 'ssl_certificate': From 3d4f822be0e3f6ff0522a48bda8e0b673bbfb26e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 16:41:23 -0700 Subject: [PATCH 086/331] Handle case where block is empty -- not sure if it ever happens, but let's not error out unnecessarily --- certbot-nginx/certbot_nginx/parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 703a33cf1..afea71c26 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -520,8 +520,7 @@ def _add_directives(block, directives, replace): """ for directive in directives: _add_directive(block, directive, replace) - last = block[-1] - if not '\n' in last: # could be " \n " or ["\n"] ! + if block and '\n' not in block[-1]: # could be " \n " or ["\n"] ! block.append(nginxparser.UnspacedList('\n')) From 0bf78242148b53420bd50456a7adb07a462c2d91 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 16 Aug 2016 17:28:12 -0700 Subject: [PATCH 087/331] fix test --- certbot/tests/client_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 4a8a8bdee..29718c263 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -67,8 +67,7 @@ class RegisterTest(unittest.TestCase): mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidEmail") with mock.patch("certbot.client.acme_client.Client") as mock_client: mock_client().register.side_effect = [mx_err, mock.MagicMock()] - self._call() - self.assertEqual(mock_get_email.call_count, 1) + self.assertRaises(errors.MissingCommandlineFlag, self._call) def test_needs_email(self): self.config.email = None From 7d0b71928c771dd48c8a3795d966fe2fa8882808 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 16 Aug 2016 17:33:17 -0700 Subject: [PATCH 088/331] incorp peter's feedback --- certbot/client.py | 8 ++++---- certbot/tests/client_test.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 03525cc0d..ef59c6ce3 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -151,10 +151,10 @@ def perform_registration(acme, config): except messages.Error as e: if e.typ == "urn:acme:error:invalidEmail": if config.noninteractive_mode: - msg = ("The email you specified was unable to be verified " - "by acme. Please ensure it is a valid email and " - "attempt registration again.") - raise errors.MissingCommandlineFlag(msg) + msg = ("The ACME server believes %s is an invalid email address. " + "Please ensure it is a valid email and attempt " + "registration again." % config.email) + raise errors.Error(msg) else: config.namespace.email = display_ops.get_email(invalid=True) return perform_registration(acme, config) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 29718c263..1ed63f466 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -61,13 +61,13 @@ class RegisterTest(unittest.TestCase): @mock.patch("certbot.account.report_new_account") @mock.patch("certbot.client.display_ops.get_email") - def test_email_retry(self, _rep, mock_get_email): + def test_email_invalid_noninteractive(self, _rep, mock_get_email): from acme import messages msg = "DNS problem: NXDOMAIN looking up MX for example.com" mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidEmail") with mock.patch("certbot.client.acme_client.Client") as mock_client: mock_client().register.side_effect = [mx_err, mock.MagicMock()] - self.assertRaises(errors.MissingCommandlineFlag, self._call) + self.assertRaises(errors.Error, self._call) def test_needs_email(self): self.config.email = None From 671d7ee19439a97b19ab215c920f925085f781e1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 17:45:43 -0700 Subject: [PATCH 089/331] Fix up COMMENT constants --- certbot-nginx/certbot_nginx/parser.py | 6 +++--- certbot-nginx/certbot_nginx/tests/parser_test.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index afea71c26..5880074f8 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -525,8 +525,8 @@ def _add_directives(block, directives, replace): REPEATABLE_DIRECTIVES = set(['server_name', 'listen', 'include']) -COMMENT_STR = ' managed by Certbot' -COMMENT = [' ', '#', ' managed by Certbot'] +COMMENT = ' managed by Certbot' +COMMENT_BLOCK = [' ', '#', COMMENT] def _comment_directive(block, location): @@ -541,7 +541,7 @@ def _comment_directive(block, location): if "Certbot" in next_entry[-1]: return next_entry = next_entry.spaced[0] # pylint: disable=no-member - block.insert(location + 1, COMMENT[:]) + block.insert(location + 1, COMMENT_BLOCK[:]) if "\n" not in next_entry: block.insert(location + 2, '\n') diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 963b4c815..fc6c8ff64 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -225,15 +225,15 @@ class NginxParserTest(util.NginxTest): ["\n", "a", " ", "b", "\n"], ["c", " ", "d"], ["\n", "e", " ", "f"]]) - from certbot_nginx.parser import _comment_directive, COMMENT + from certbot_nginx.parser import _comment_directive, COMMENT_BLOCK _comment_directive(block, 1) _comment_directive(block, 0) self.assertEqual(block.spaced, [ ["\n", "a", " ", "b", "\n"], - COMMENT, + COMMENT_BLOCK, "\n", ["c", " ", "d"], - COMMENT, + COMMENT_BLOCK, ["\n", "e", " ", "f"]]) def test_get_all_certs_keys(self): From ceb5207d56758feb72edfd02c7e56c9c2692ca17 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 16 Aug 2016 18:06:21 -0700 Subject: [PATCH 090/331] lint --- certbot/tests/client_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 1ed63f466..7ff46be05 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -60,8 +60,7 @@ class RegisterTest(unittest.TestCase): self._call() @mock.patch("certbot.account.report_new_account") - @mock.patch("certbot.client.display_ops.get_email") - def test_email_invalid_noninteractive(self, _rep, mock_get_email): + def test_email_invalid_noninteractive(self, _rep): from acme import messages msg = "DNS problem: NXDOMAIN looking up MX for example.com" mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidEmail") From 76c2fe579a1c9d49f9ebca6e716b4a2eec76b9d9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 18:30:45 -0700 Subject: [PATCH 091/331] Make _comment_directive more defensive --- certbot-nginx/certbot_nginx/parser.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 5880074f8..d98b5225d 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -531,18 +531,17 @@ COMMENT_BLOCK = [' ', '#', COMMENT] def _comment_directive(block, location): """Add a comment to the end of the line at location.""" - if len(block) > location + 1: # there is a block after us - next_entry = block[location + 1] - else: - # we're at the end of the block, pretend there's a newline after us; - # it will actually be added later in add_directives - next_entry = "\n" - if isinstance(next_entry, list): - if "Certbot" in next_entry[-1]: + next_entry = block[location + 1] if location + 1 < len(block) else None + if isinstance(next_entry, list) and next_entry: + if COMMENT in next_entry[-1]: return - next_entry = next_entry.spaced[0] # pylint: disable=no-member + elif isinstance(next_entry, nginxparser.UnspacedList): + next_entry = next_entry.spaced[0] + else: + next_entry = next_entry[0] + block.insert(location + 1, COMMENT_BLOCK[:]) - if "\n" not in next_entry: + if next_entry is not None and "\n" not in next_entry: block.insert(location + 2, '\n') From 76d17bfd0f84fb42e71fe262cb056def533a71b1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 18:40:05 -0700 Subject: [PATCH 092/331] Avoid modifying parsed ssl_options --- certbot-nginx/certbot_nginx/configurator.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 5e415bce6..1a14a3866 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -338,16 +338,14 @@ class NginxConfigurator(common.Plugin): """ snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() - options_subblock = self.parser.loc["ssl_options"] # the options file doesn't have a newline at the beginning, but there # needs to be one when it's dropped into the file - if options_subblock and "\n" not in options_subblock[0]: - options_subblock[0].insert(0, "\n") ssl_block = ( [['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)], ['\n ', 'ssl_certificate', ' ', snakeoil_cert], - ['\n ', 'ssl_certificate_key', ' ', snakeoil_key]] + - options_subblock) + ['\n ', 'ssl_certificate_key', ' ', snakeoil_key], + ['\n']] + + self.parser.loc["ssl_options"]) self.parser.add_server_directives( vhost.filep, vhost.names, ssl_block, replace=False) From 015d103f8d90bc7a3fa1e94f13013702c1fc9ec5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 16 Aug 2016 18:46:19 -0700 Subject: [PATCH 093/331] Unbreak using.html#plugins links Fixes: #3422 --- docs/using.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/using.rst b/docs/using.rst index 03b4cc6f3..92bf59000 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -231,6 +231,7 @@ whole process is described in the :doc:`contributing`. corrupt your operating system and are **not supported** by the Certbot team! +.. _plugins: Getting certificates ==================== From 971d6d75401fa228468ea376829dcdb6072736da Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 18:50:18 -0700 Subject: [PATCH 094/331] Don't hardcode comment added by Certbot --- certbot-nginx/certbot_nginx/tests/parser_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index fc6c8ff64..71807d4f4 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -141,12 +141,13 @@ class NginxParserTest(util.NginxTest): replace=False) nparser.add_server_directives(server_conf, names, [['foo', 'bar']], replace=False) + from certbot_nginx.parser import COMMENT self.assertEqual(nparser.parsed[server_conf], [['server_name', 'somename alias another.alias'], ['foo', 'bar'], - ['#', ' managed by Certbot'], + ['#', COMMENT], ['ssl_certificate', '/etc/ssl/cert2.pem'], - ['#', ' managed by Certbot'], + ['#', COMMENT], [], [] ]) @@ -173,11 +174,12 @@ class NginxParserTest(util.NginxTest): filep = nparser.abs_path('sites-enabled/example.com') nparser.add_server_directives( filep, target, [['server_name', 'foobar.com']], replace=True) + from certbot_nginx.parser import COMMENT self.assertEqual( nparser.parsed[filep], [[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], - ['server_name', 'foobar.com'], ['#', ' managed by Certbot'], + ['server_name', 'foobar.com'], ['#', COMMENT], ['server_name', 'example.*'], [] ]]]) self.assertRaises(errors.MisconfigurationError, From 5ec22438ff24579736d94a4fa831cb3ab015866d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 19:04:05 -0700 Subject: [PATCH 095/331] Make sure mod_ssl_conf exists so it can be parsed --- certbot-nginx/certbot_nginx/configurator.py | 4 ++-- certbot-nginx/tests/boulder-integration.sh | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 1a14a3866..0383e0693 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -117,6 +117,8 @@ class NginxConfigurator(common.Plugin): # Make sure configuration is valid self.config_test() + # temp_install must be run before creating the NginxParser + temp_install(self.mod_ssl_conf) self.parser = parser.NginxParser( self.conf('server-root'), self.mod_ssl_conf) @@ -124,8 +126,6 @@ class NginxConfigurator(common.Plugin): if self.version is None: self.version = self.get_version() - temp_install(self.mod_ssl_conf) - # Entry point in main.py for installing cert def deploy_cert(self, domain, cert_path, key_path, chain_path=None, fullchain_path=None): diff --git a/certbot-nginx/tests/boulder-integration.sh b/certbot-nginx/tests/boulder-integration.sh index 58613d86f..bd35aee21 100755 --- a/certbot-nginx/tests/boulder-integration.sh +++ b/certbot-nginx/tests/boulder-integration.sh @@ -7,7 +7,6 @@ export PATH="/usr/sbin:$PATH" # /usr/sbin/nginx nginx_root="$root/nginx" mkdir $nginx_root root="$nginx_root" ./certbot-nginx/tests/boulder-integration.conf.sh > $nginx_root/nginx.conf -cp certbot-nginx/certbot_nginx/options-ssl-nginx.conf "$root"/conf killall nginx || true nginx -c $nginx_root/nginx.conf From 1aa18a3bad5fb4d435b967a6dcb4cdc5f34809d5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 19:10:57 -0700 Subject: [PATCH 096/331] Add test to prevent regressing and not copying ssl_options to /etc/letsencrypt --- certbot-nginx/certbot_nginx/tests/configurator_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 800387e57..ec7408f27 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -39,6 +39,8 @@ class NginxConfiguratorTest(util.NginxTest): def test_prepare(self): self.assertEqual((1, 6, 2), self.config.version) self.assertEqual(5, len(self.config.parser.parsed)) + # ensure we successfully parsed a file for ssl_options + self.assertTrue(self.config.parser.loc["ssl_options"]) @mock.patch("certbot_nginx.configurator.util.exe_exists") @mock.patch("certbot_nginx.configurator.subprocess.Popen") From 465aa38143e67d00befda3a59e51c943b1c4f6fb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 19:33:19 -0700 Subject: [PATCH 097/331] Revert "Catch all pyparsing exceptions" This reverts commit 7fb5cf1cf54c5c5ab10f368d0798ca0fa5164792. --- certbot-nginx/certbot_nginx/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index d98b5225d..681fadc55 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -169,7 +169,7 @@ class NginxParser(object): trees.append(parsed) except IOError: logger.warning("Could not open file: %s", item) - except pyparsing.ParseBaseException: + except pyparsing.ParseException: logger.debug("Could not parse file: %s", item) return trees From 449487e8cbcf4dce88c8ffd6a7d8f64cab3cd0c3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 19:34:16 -0700 Subject: [PATCH 098/331] Catch all pyparsing exceptions --- certbot-nginx/certbot_nginx/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 681fadc55..90bb49aaf 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -180,7 +180,7 @@ class NginxParser(object): return nginxparser.load(_file).spaced except IOError: logger.warn("Missing NGINX TLS options file: %s", ssl_options) - except pyparsing.ParseException: + except pyparsing.ParseBaseException: logger.debug("Could not parse file: %s", ssl_options) return [] From 73fdc08d8348335a883f9a21a00ed082f59e3057 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2016 21:04:28 -0700 Subject: [PATCH 099/331] don't hardcode certbot comment --- .../certbot_nginx/tests/configurator_test.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index ec7408f27..9e0c0dda5 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -13,6 +13,7 @@ from acme import messages from certbot import achallenges from certbot import errors +from certbot_nginx import parser from certbot_nginx.tests import util @@ -91,14 +92,13 @@ class NginxConfiguratorTest(util.NginxTest): # pylint: disable=protected-access parsed = self.config.parser._parse_files(filep, override=True) - self.assertEqual([[['server'], [ - ['listen', '69.50.225.155:9000'], - ['listen', '127.0.0.1'], - ['server_name', '.example.com'], - ['server_name', 'example.*'], - ['listen', '5001 ssl'], - ['#', ' managed by Certbot'] - ]]], + self.assertEqual([[['server'], + [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + ['listen', '5001 ssl'], + ['#', parser.COMMENT]]]], parsed[0]) def test_choose_vhost(self): From 0c0603b9ea33ef95a13e676668c6cf3ef0204e11 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 17 Aug 2016 14:58:14 -0700 Subject: [PATCH 100/331] copy peter's OS update below --- docs/using.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 2fc25685e..4dabd0d3e 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -286,8 +286,10 @@ the circumstances in which each plugin can be used, and how to use it. Apache ------ -If you're running Apache 2.4 on a Debian-based OS with version 1.0+ of -the ``libaugeas0`` package available, you can use the Apache plugin. +The Apache plugin currently requires OS with augeas version 1.0; currently `it +supports +`_ +modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin. This automates both obtaining *and* installing certs on an Apache webserver. To specify this plugin on the command line, simply include ``--apache``. From 44cf40472e206979d71e5b396cb7bec12d8c30c4 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 17 Aug 2016 15:43:35 -0700 Subject: [PATCH 101/331] add allow subset --- docs/using.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 4dabd0d3e..20b6cc5c7 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -262,7 +262,8 @@ Plugins that can install a cert are called "installers" and can be used with the serve your website over HTTPS using certificates obtained by certbot. Plugins that do both can be used with the "certbot run" command, which is the default -when no command is specified. +when no command is specified. The "run" subcommand can also be used to specify +a combination of distinct authenticator and installer plugins. =========== ==== ==== =============================================================== Plugin Auth Inst Notes @@ -438,6 +439,11 @@ do not want this behavior. certificate that contains all of the old domains and one or more additional new domains. +``--allow-subset-of-names`` tells Certbot to continue with cert generation if +only some of the specified domain authorazations can be obtained. This may +be useful if some domains specified in a certificate no longer point at this +system. + Whenever you obtain a new certificate in any of these ways, the new certificate exists alongside any previously-obtained certificates, whether or not the previous certificates have expired. The generation of a new From 93047d6579c4869cd4896f15b4d9ddbf47ddcbe5 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 17 Aug 2016 15:49:19 -0700 Subject: [PATCH 102/331] add back in email test --- certbot/tests/client_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 7ff46be05..98d853716 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -59,6 +59,18 @@ class RegisterTest(unittest.TestCase): with mock.patch("certbot.account.report_new_account"): self._call() + @mock.patch("certbot.account.report_new_account") + @mock.patch("certbot.client.display_ops.get_email") + def test_email_retry(self, _rep, mock_get_email): + from acme import messages + self.config.noninteractive_mode = False + msg = "DNS problem: NXDOMAIN looking up MX for example.com" + mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidEmail") + with mock.patch("certbot.client.acme_client.Client") as mock_client: + mock_client().register.side_effect = [mx_err, mock.MagicMock()] + self._call() + self.assertEqual(mock_get_email.call_count, 1) + @mock.patch("certbot.account.report_new_account") def test_email_invalid_noninteractive(self, _rep): from acme import messages From 9333be6c8856f1c8c1681f90ea4dd13834e34828 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 17 Aug 2016 16:07:37 -0700 Subject: [PATCH 103/331] Add pyparsing hashes to requirements file --- .../pieces/letsencrypt-auto-requirements.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 0c642a33e..d9b51ec03 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -117,6 +117,15 @@ pyasn1==0.1.9 \ pyopenssl==16.0.0 \ --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d +pyparsing==2.1.8 \ + --hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \ + --hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \ + --hash=sha256:ab09aee814c0241ff0c503cff30018219fe1fc14501d89f406f4664a0ec9fbcd \ + --hash=sha256:6e9a7f052f8e26bcf749e4033e3115b6dc7e3c85aafcb794b9a88c9d9ef13c97 \ + --hash=sha256:9f463a6bcc4eeb6c08f1ed84439b17818e2085937c0dee0d7674ac127c67c12b \ + --hash=sha256:3626b4d81cfb300dad57f52f2f791caaf7b06c09b368c0aa7b868e53a5775424 \ + --hash=sha256:367b90cc877b46af56d4580cd0ae278062903f02b8204ab631f5a2c0f50adfd0 \ + --hash=sha256:9f1ea360086cd68681e7f4ca8f1f38df47bf81942a0d76a9673c2d23eff35b13 pyRFC3339==1.0 \ --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 From a89dfc7226e58b547ca6915f20c90eec0e52035e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 17 Aug 2016 16:10:21 -0700 Subject: [PATCH 104/331] Add the nginx plugin's hash to certbot-auto during the release process --- tools/release.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/release.sh b/tools/release.sh index c883e3d61..7747b0e2b 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -164,13 +164,13 @@ for module in certbot $subpkgs_modules ; do done # pin pip hashes of the things we just built -for pkg in acme certbot certbot-apache ; do +for pkg in acme certbot certbot-apache certbot-nginx ; do echo $pkg==$version \\ pip hash dist."$version/$pkg"/*.{whl,gz} | grep "^--hash" | python2 -c 'from sys import stdin; input = stdin.read(); print " ", input.replace("\n--hash", " \\\n --hash"),' done > /tmp/hashes.$$ deactivate -if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*9 " ; then +if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*12 " ; then echo Unexpected pip hash output exit 1 fi From 6dce950d6ddd27053c2a7b40599ddf9aa8ef7995 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 17 Aug 2016 16:12:12 -0700 Subject: [PATCH 105/331] Update comment about how to generate requirements file --- .../pieces/letsencrypt-auto-requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index d9b51ec03..342aa2f88 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -1,6 +1,7 @@ # This is the flattened list of packages certbot-auto installs. To generate -# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and -# then use `hashin` or a more secure method to gather the hashes. +# this, do +# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`, +# and then use `hashin` or a more secure method to gather the hashes. argparse==1.4.0 \ --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ From 4e1830b372d9ca0c3c9bb8244071723e5336c1c2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 17 Aug 2016 16:27:23 -0700 Subject: [PATCH 106/331] hide the nginx plugin --- certbot-nginx/certbot_nginx/configurator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index a1c24b5c8..298bf7f69 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -57,6 +57,8 @@ class NginxConfigurator(common.Plugin): description = "Nginx Web Server - currently doesn't work" + hidden = True + @classmethod def add_parser_arguments(cls, add): add("server-root", default=constants.CLI_DEFAULTS["server_root"], From 9fd003cd664edbf17b866a850206cfaeb0062226 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 17 Aug 2016 16:37:01 -0700 Subject: [PATCH 107/331] Mark the Nginx plugin as alpha --- certbot-nginx/certbot_nginx/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 298bf7f69..049ba9a20 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -55,7 +55,7 @@ class NginxConfigurator(common.Plugin): """ - description = "Nginx Web Server - currently doesn't work" + description = "Nginx Web Server plugin - Alpha" hidden = True From 5c16b43221a903cac615287998e4c9f6772909ac Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 17 Aug 2016 17:00:51 -0700 Subject: [PATCH 108/331] satisfy OCD by removing space --- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 4abe7be38..5aa49ccf2 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -285,7 +285,7 @@ BootstrapRpmCommon() { yes_flag="-y" fi - if ! $SUDO $tool list *virtualenv > /dev/null 2>&1; then + if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." if [ "$ASSUME_YES" = 1 ]; then /bin/echo -n "Enabling the EPEL repository in 3 seconds..." diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index e9865aed3..3c12a5f4d 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -21,7 +21,7 @@ BootstrapRpmCommon() { yes_flag="-y" fi - if ! $SUDO $tool list *virtualenv > /dev/null 2>&1; then + if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." if [ "$ASSUME_YES" = 1 ]; then /bin/echo -n "Enabling the EPEL repository in 3 seconds..." From 156c6415c2a6472e5e96a3e1150d399fb0b6ab74 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 17 Aug 2016 17:31:56 -0700 Subject: [PATCH 109/331] error out when we can't simply install epel-release --- letsencrypt-auto-source/letsencrypt-auto | 4 ++++ letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 5aa49ccf2..7c851ffc4 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -287,6 +287,10 @@ BootstrapRpmCommon() { if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." + if ! $SUDO $tool list epel-release >/dev/null 2>&1; then + echo "Please enable this repository and try running Certbot again." + exit 1 + fi if [ "$ASSUME_YES" = 1 ]; then /bin/echo -n "Enabling the EPEL repository in 3 seconds..." sleep 1s diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 3c12a5f4d..2fd629ff8 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -23,6 +23,10 @@ BootstrapRpmCommon() { if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." + if ! $SUDO $tool list epel-release >/dev/null 2>&1; then + echo "Please enable this repository and try running Certbot again." + exit 1 + fi if [ "$ASSUME_YES" = 1 ]; then /bin/echo -n "Enabling the EPEL repository in 3 seconds..." sleep 1s From 5500004dd60c375efcf44d0afb86e428b42a4ed1 Mon Sep 17 00:00:00 2001 From: Jeroen Pluimers Date: Thu, 18 Aug 2016 10:38:47 +0200 Subject: [PATCH 110/331] Fix the links for #3416 and align `README.rst` & `docs/resources.rst` Fix the links for #3416 and align content of in README.rst and docs/resources.rst so it's easier to later de-dupe (I've not done this now as in README.rst does too much tag: fiddling and I'm not sure how that will work out if the fiddling is not aware of .. include::. --- README.rst | 8 +++++++- docs/resources.rst | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 0b8e652c0..174dc7dcc 100644 --- a/README.rst +++ b/README.rst @@ -30,10 +30,12 @@ If you'd like to contribute to this project please read `Developer Guide Installation ------------ -The easiest way to install Certbot is by visiting https://certbot.eff.org, where you can +The easiest way to install Certbot is by visiting `certbot.eff.org`_, where you can find the correct installation instructions for many web server and OS combinations. For more information, see the `User Guide `_. +.. _certbot.eff.org: https://certbot.eff.org/ + How to run the client --------------------- @@ -94,6 +96,10 @@ ACME working area in github: https://github.com/ietf-wg-acme/acme Mailing list: `client-dev`_ (to subscribe without a Google account, send an email to client-dev+subscribe@letsencrypt.org) +.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt +.. _OFTC: https://webchat.oftc.net?channels=%23certbot +.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev + |build-status| |coverage| |docs| |container| diff --git a/docs/resources.rst b/docs/resources.rst index a284f4a3d..f10aa2920 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -24,6 +24,10 @@ ACME working area in github: https://github.com/ietf-wg-acme/acme Mailing list: `client-dev`_ (to subscribe without a Google account, send an email to client-dev+subscribe@letsencrypt.org) +.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt +.. _OFTC: https://webchat.oftc.net?channels=%23certbot +.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev + |build-status| |coverage| |docs| |container| @@ -45,10 +49,6 @@ email to client-dev+subscribe@letsencrypt.org) :alt: Docker Repository on Quay.io .. _`installation instructions`: - https://letsencrypt.readthedocs.org/en/latest/using.html + https://letsencrypt.readthedocs.org/en/latest/using.html#getting-certbot .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU - -.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt -.. _OFTC: https://webchat.oftc.net?channels=%23certbot -.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev From 075e66630030ea7f2a33afbe3127e8b12d5e5bc1 Mon Sep 17 00:00:00 2001 From: Mathieu Leduc-Hamel Date: Thu, 18 Aug 2016 09:54:30 -0400 Subject: [PATCH 111/331] Fixes broken tests --- certbot/plugins/manual.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index b3cb2bec3..a42d3d2ce 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -195,7 +195,7 @@ s.serve_forever()" """ uri=uri, command=command) - self._ip_logging_permission(response, formated_message) + self._ip_logging_permission(formated_message) if not response.simple_verify( achall.chall, achall.domain, @@ -211,7 +211,7 @@ s.serve_forever()" """ formated_message = message.format(validation=validation, domain=achall.domain, response=response) - self._ip_logging_permission(response, formated_message) + self._ip_logging_permission(formated_message) if not response.simple_verify( achall.chall, achall.domain, @@ -241,7 +241,7 @@ s.serve_forever()" """ sys.stdout.write(message) six.moves.input("Press ENTER to continue") - def _ip_logging_permission(self, response, formated_message): + def _ip_logging_permission(self, formated_message): # pylint: disable=missing-docstring if not self.conf("public-ip-logging-ok"): if not zope.component.getUtility(interfaces.IDisplay).yesno( From df61b0e3497412238f841abb07e8ef148bbb4ba5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 18 Aug 2016 13:56:15 -0700 Subject: [PATCH 112/331] Check for comments more accurately --- certbot-nginx/certbot_nginx/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 90bb49aaf..3919858d9 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -533,7 +533,7 @@ def _comment_directive(block, location): """Add a comment to the end of the line at location.""" next_entry = block[location + 1] if location + 1 < len(block) else None if isinstance(next_entry, list) and next_entry: - if COMMENT in next_entry[-1]: + if len(next_entry) >= 2 and next_entry[-2] == "#" and COMMENT in next_entry[-1]: return elif isinstance(next_entry, nginxparser.UnspacedList): next_entry = next_entry.spaced[0] From 702ed89007b7458d08bd531a9c2e2879d10f2ee5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 18 Aug 2016 14:49:11 -0700 Subject: [PATCH 113/331] use six instead of custom refresh function --- certbot/plugins/util_test.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index e1a064fb3..63a472fa0 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -4,13 +4,7 @@ import unittest import sys import mock - -try: - # Python 3.5+ - from importlib import reload as refresh # pylint: disable=no-name-in-module -except ImportError: - # Python 2-3.4 - from imp import reload as refresh +from six.moves import reload_module # pylint: disable=import-error class PathSurgeryTest(unittest.TestCase): @@ -50,14 +44,14 @@ class AlreadyListeningTestNoPsutil(unittest.TestCase): sys.modules['psutil'] = None # Reload hackery to ensure getting non-psutil version # loaded to memory - refresh(certbot.plugins.util) + reload_module(certbot.plugins.util) def tearDown(self): # Need to reload the module to ensure # getting back to normal import certbot.plugins.util sys.modules["psutil"] = self.psutil - refresh(certbot.plugins.util) + reload_module(certbot.plugins.util) @mock.patch("certbot.plugins.util.zope.component.getUtility") def test_ports_available(self, mock_getutil): From 9958a7fc1cb78a72823288f0978ea1459548bc8c Mon Sep 17 00:00:00 2001 From: Mathieu Leduc-Hamel Date: Thu, 18 Aug 2016 14:55:03 -0400 Subject: [PATCH 114/331] Handle missing dnspython by displaying a warning message --- acme/acme/challenges.py | 4 ++-- acme/acme/errors.py | 4 ++++ certbot/plugins/manual.py | 11 +++++++++-- certbot/plugins/manual_test.py | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 6242c376c..4ebd37bf9 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -234,8 +234,8 @@ class DNS01Response(KeyAuthorizationChallengeResponse): try: from acme import dns_resolver except ImportError: # pragma: no cover - raise errors.Error("Local validation for 'dns-01' challenges " - "requires 'dnspython'") + raise errors.DependencyError("Local validation for 'dns-01' " + "challenges requires 'dnspython'") txt_records = dns_resolver.txt_records_for_name(validation_domain_name) exists = validation in txt_records if not exists: diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 70894a808..7446b60fc 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -6,6 +6,10 @@ class Error(Exception): """Generic ACME error.""" +class DependencyError(Error): + """Dependency error""" + + class SchemaValidationError(jose_errors.DeserializationError): """JSON schema ACME object validation error.""" diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index a42d3d2ce..a7ba571d7 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -15,6 +15,7 @@ import zope.interface from functools import partial from acme import challenges +from acme import errors as acme_errors from certbot import errors from certbot import interfaces @@ -213,9 +214,15 @@ s.serve_forever()" """ response=response) self._ip_logging_permission(formated_message) - if not response.simple_verify( + try: + verification_status = response.simple_verify( achall.chall, achall.domain, - achall.account_key.public_key()): + achall.account_key.public_key()) + except acme_errors.DependencyError: + verification_status = False + logger.warning("Dns challenge requires `dnspython`") + + if not verification_status: logger.warning("Self-verify of challenge failed.") return response diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index 0686a24d7..0179c2932 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -5,6 +5,7 @@ import unittest import mock from acme import challenges +from acme import errors as acme_errors from acme import jose from certbot import achallenges @@ -78,6 +79,19 @@ class AuthenticatorTest(unittest.TestCase): self.auth.perform(self.achalls) self.assertEqual(2, mock_logger.warning.call_count) + @mock.patch("certbot.plugins.manual.zope.component.getUtility") + @mock.patch("acme.challenges.DNS01Response.simple_verify") + @mock.patch("six.moves.input") + def test_perform_missing_dependency(self, mock_raw_input, mock_verify, mock_interaction): + mock_interaction().yesno.return_value = True + mock_verify.side_effect = acme_errors.DependencyError() + + with mock.patch("certbot.plugins.manual.logger") as mock_logger: + self.auth.perform([self.dns01]) + self.assertEqual(2, mock_logger.warning.call_count) + + mock_raw_input.assert_called_once_with("Press ENTER to continue") + @mock.patch("certbot.plugins.manual.zope.component.getUtility") @mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait") def test_disagree_with_ip_logging(self, mock_notify, mock_interaction): From 2c411056fab281437dcaefe4bd4f9508109567a2 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 19 Aug 2016 11:54:35 -0700 Subject: [PATCH 115/331] Remove obsolete test. --- acme/acme/client_test.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index a526a0984..585576e2d 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -102,12 +102,6 @@ class ClientTest(unittest.TestCase): self.assertEqual(self.regr, self.client.register(self.new_reg)) # TODO: test POST call arguments - # TODO: split here and separate test - reg_wrong_key = self.regr.body.update(key=KEY2.public_key()) - self.response.json.return_value = reg_wrong_key.to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.client.register, self.new_reg) - def test_register_missing_next(self): self.response.status_code = http_client.CREATED self.assertRaises( From df68b44d38f99a0cb80dd7056d657ec00d4587f5 Mon Sep 17 00:00:00 2001 From: DanCld Date: Sun, 21 Aug 2016 21:50:14 +0300 Subject: [PATCH 116/331] Fix apache logs dir for centos --- certbot-apache/certbot_apache/configurator.py | 7 +++++-- certbot-apache/certbot_apache/constants.py | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 17ff6c8db..30642af52 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -98,6 +98,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): help="Apache server root directory.") add("vhost-root", default=constants.os_constant("vhost_root"), help="Apache server VirtualHost configuration root") + add("logs-root", default=constants.os_constant("logs_root"), + help="Apache server logs directory") add("challenge-location", default=constants.os_constant("challenge_location"), help="Directory path for challenge configuration.") @@ -1425,13 +1427,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "RewriteEngine On\n" "RewriteRule %s\n" "\n" - "ErrorLog /var/log/apache2/redirect.error.log\n" + "ErrorLog %s/redirect.error.log\n" "LogLevel warn\n" "\n" % (" ".join(str(addr) for addr in self._get_proposed_addrs(ssl_vhost)), servername, serveralias, - " ".join(rewrite_rule_args))) + " ".join(rewrite_rule_args), + self.conf("logs-root") )) def _write_out_redirect(self, ssl_vhost, text): # This is the default name diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index ba545c613..dcc635c4b 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -6,6 +6,7 @@ CLI_DEFAULTS_DEFAULT = dict( server_root="/etc/apache2", vhost_root="/etc/apache2/sites-available", vhost_files="*", + logs_root="/var/log/apache2", version_cmd=['apache2ctl', '-v'], define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], restart_cmd=['apache2ctl', 'graceful'], @@ -23,6 +24,7 @@ CLI_DEFAULTS_DEBIAN = dict( server_root="/etc/apache2", vhost_root="/etc/apache2/sites-available", vhost_files="*", + logs_root="/var/log/apache2", version_cmd=['apache2ctl', '-v'], define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], restart_cmd=['apache2ctl', 'graceful'], @@ -40,6 +42,7 @@ CLI_DEFAULTS_CENTOS = dict( server_root="/etc/httpd", vhost_root="/etc/httpd/conf.d", vhost_files="*.conf", + logs_root="/var/log/httpd", version_cmd=['apachectl', '-v'], define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'], restart_cmd=['apachectl', 'graceful'], @@ -57,6 +60,7 @@ CLI_DEFAULTS_GENTOO = dict( server_root="/etc/apache2", vhost_root="/etc/apache2/vhosts.d", vhost_files="*.conf", + logs_root="/var/log/apache2", version_cmd=['/usr/sbin/apache2', '-v'], define_cmd=['apache2ctl', 'virtualhosts'], restart_cmd=['apache2ctl', 'graceful'], @@ -74,6 +78,7 @@ CLI_DEFAULTS_DARWIN = dict( server_root="/etc/apache2", vhost_root="/etc/apache2/other", vhost_files="*.conf", + logs_root="/var/log/apache2", version_cmd=['/usr/sbin/httpd', '-v'], define_cmd=['/usr/sbin/httpd', '-t', '-D', 'DUMP_RUN_CFG'], restart_cmd=['apachectl', 'graceful'], @@ -91,6 +96,7 @@ CLI_DEFAULTS_SUSE = dict( server_root="/etc/apache2", vhost_root="/etc/apache2/vhosts.d", vhost_files="*.conf", + logs_root="/var/log/apache2", version_cmd=['apache2ctl', '-v'], define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'], restart_cmd=['apache2ctl', 'graceful'], From ed7c022565a9ee7290fbab35e81c176d88578d04 Mon Sep 17 00:00:00 2001 From: DanCld Date: Mon, 22 Aug 2016 08:16:20 +0300 Subject: [PATCH 117/331] Lint fix, space before parentheses --- certbot-apache/certbot_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 30642af52..75fbe3456 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -1434,7 +1434,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): addr in self._get_proposed_addrs(ssl_vhost)), servername, serveralias, " ".join(rewrite_rule_args), - self.conf("logs-root") )) + self.conf("logs-root"))) def _write_out_redirect(self, ssl_vhost, text): # This is the default name From 3fe5d9c3e0bd92ea82a2c6c1cf92baff79ed5e48 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 23 Aug 2016 11:39:35 -0700 Subject: [PATCH 118/331] add custom skipUnless function --- certbot/plugins/util_test.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index 63a472fa0..dcb21e037 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -75,6 +75,42 @@ class AlreadyListeningTestNoPsutil(unittest.TestCase): self.assertEqual(mock_getutil.call_count, 2) +def psutil_available(): + """Checks if psutil can be imported. + + :rtype: bool + :returns: True iff psutil is installed and can be imported + + """ + try: + import psutil # pylint: disable=unused-variable + except ImportError: + return False + return True + + +def skipUnless(condition, reason): + """Skip tests unless a condition holds. + + This implements the basic functionality of unittest.skipUnless + which is only available on Python 2.7+. + + :param bool condition: skip the test iff condition is False + :param str reason: the reason for skipping the test + + :rtype: function + :returns: a decorator that will hide tests unless condition is True + + """ + if hasattr(unittest, "skipUnless"): + return unittest.skipUnless(condition, reason) + elif condition: + return lambda x: x + else: + return lambda x: None + + +@skipUnless(psutil_available(), "optional dependency psutil is not available") class AlreadyListeningTestPsutil(unittest.TestCase): """Tests for certbot.plugins.already_listening.""" def _call(self, *args, **kwargs): From a69ad2afa80bbdd63e32f90e3bfe78d79c33eefe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 23 Aug 2016 11:52:28 -0700 Subject: [PATCH 119/331] Give lambda parameter a more useful name --- certbot/plugins/util_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index dcb21e037..cb17cebe2 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -105,9 +105,9 @@ def skipUnless(condition, reason): if hasattr(unittest, "skipUnless"): return unittest.skipUnless(condition, reason) elif condition: - return lambda x: x + return lambda cls: cls else: - return lambda x: None + return lambda cls: None @skipUnless(psutil_available(), "optional dependency psutil is not available") From ad45a664a856b21083dcb5a2e31a3f89ea7823a2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Aug 2016 08:26:23 -0700 Subject: [PATCH 120/331] use "iff" iff it is common international shorthand --- certbot/plugins/util_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index cb17cebe2..58dcfdd38 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -79,7 +79,7 @@ def psutil_available(): """Checks if psutil can be imported. :rtype: bool - :returns: True iff psutil is installed and can be imported + :returns: True if psutil can be imported, otherwise, False """ try: @@ -95,7 +95,7 @@ def skipUnless(condition, reason): This implements the basic functionality of unittest.skipUnless which is only available on Python 2.7+. - :param bool condition: skip the test iff condition is False + :param bool condition: If False, the test will be skipped :param str reason: the reason for skipping the test :rtype: function From 80bcf9a67863ffebcd8685ed264f19f4e0c8ff62 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Aug 2016 08:35:53 -0700 Subject: [PATCH 121/331] make docstring prettier when converted to html --- certbot/plugins/util_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index 58dcfdd38..27ede6533 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -79,7 +79,7 @@ def psutil_available(): """Checks if psutil can be imported. :rtype: bool - :returns: True if psutil can be imported, otherwise, False + :returns: ``True`` if psutil can be imported, otherwise, ``False`` """ try: @@ -95,11 +95,11 @@ def skipUnless(condition, reason): This implements the basic functionality of unittest.skipUnless which is only available on Python 2.7+. - :param bool condition: If False, the test will be skipped + :param bool condition: If ``False``, the test will be skipped :param str reason: the reason for skipping the test - :rtype: function - :returns: a decorator that will hide tests unless condition is True + :rtype: callable + :returns: decorator that hides tests unless condition is ``True`` """ if hasattr(unittest, "skipUnless"): From ff6d1f9fc91ed34287fa81d6dddfda1aa93b57f9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Aug 2016 14:32:47 -0700 Subject: [PATCH 122/331] Add tags around link text --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 174dc7dcc..23337811d 100644 --- a/README.rst +++ b/README.rst @@ -76,6 +76,8 @@ the User Guide. Links ===== +.. Do not modify this comment unless you know what you're doing. tag:links-begin + Documentation: https://certbot.eff.org/docs Software project: https://github.com/certbot/certbot @@ -125,6 +127,8 @@ email to client-dev+subscribe@letsencrypt.org) .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU +.. Do not modify this comment unless you know what you're doing. tag:links-end + System Requirements =================== From 85bd78a81b1bbc934d6abe245f7b4377d282bbf8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Aug 2016 14:34:39 -0700 Subject: [PATCH 123/331] cleanup links --- README.rst | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 23337811d..74dc870ed 100644 --- a/README.rst +++ b/README.rst @@ -84,7 +84,9 @@ Software project: https://github.com/certbot/certbot Notes for developers: https://certbot.eff.org/docs/contributing.html -Main Website: https://letsencrypt.org/ +Main Website: https://certbot.eff.org + +Let's Encrypt Website: https://letsencrypt.org IRC Channel: #letsencrypt on `Freenode`_ or #certbot on `OFTC`_ @@ -98,13 +100,13 @@ ACME working area in github: https://github.com/ietf-wg-acme/acme Mailing list: `client-dev`_ (to subscribe without a Google account, send an email to client-dev+subscribe@letsencrypt.org) -.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt -.. _OFTC: https://webchat.oftc.net?channels=%23certbot -.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev - |build-status| |coverage| |docs| |container| +.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt +.. _OFTC: https://webchat.oftc.net?channels=%23certbot + +.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev .. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master :target: https://travis-ci.org/certbot/certbot @@ -122,11 +124,6 @@ email to client-dev+subscribe@letsencrypt.org) :target: https://quay.io/repository/letsencrypt/letsencrypt :alt: Docker Repository on Quay.io -.. _`installation instructions`: - https://letsencrypt.readthedocs.org/en/latest/using.html#getting-certbot - -.. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU - .. Do not modify this comment unless you know what you're doing. tag:links-end System Requirements From 122e05a2ff31a309ce36f1b2716443f0e617cb44 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Aug 2016 14:37:47 -0700 Subject: [PATCH 124/331] Remove duplication in resources.rst --- docs/resources.rst | 53 +++------------------------------------------- 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/docs/resources.rst b/docs/resources.rst index f10aa2920..459d8a829 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -2,53 +2,6 @@ Resources ===================== -Documentation: https://certbot.eff.org/docs - -Software project: https://github.com/certbot/certbot - -Notes for developers: https://certbot.eff.org/docs/contributing.html - -Main Website: https://letsencrypt.org/ - -Let's Encrypt FAQ: https://community.letsencrypt.org/t/frequently-asked-questions-faq/26#topic-title - -IRC Channel: #letsencrypt on `Freenode`_ or #certbot on `OFTC`_ - -Community: https://community.letsencrypt.org - -ACME spec: http://ietf-wg-acme.github.io/acme/ - -ACME working area in github: https://github.com/ietf-wg-acme/acme - - -Mailing list: `client-dev`_ (to subscribe without a Google account, send an -email to client-dev+subscribe@letsencrypt.org) - -.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt -.. _OFTC: https://webchat.oftc.net?channels=%23certbot -.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev - -|build-status| |coverage| |docs| |container| - - - -.. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master - :target: https://travis-ci.org/certbot/certbot - :alt: Travis CI status - -.. |coverage| image:: https://coveralls.io/repos/certbot/certbot/badge.svg?branch=master - :target: https://coveralls.io/r/certbot/certbot - :alt: Coverage status - -.. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ - :target: https://readthedocs.org/projects/letsencrypt/ - :alt: Documentation status - -.. |container| image:: https://quay.io/repository/letsencrypt/letsencrypt/status - :target: https://quay.io/repository/letsencrypt/letsencrypt - :alt: Docker Repository on Quay.io - -.. _`installation instructions`: - https://letsencrypt.readthedocs.org/en/latest/using.html#getting-certbot - -.. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU +.. include:: ../README.rst + :start-after: tag:links-begin + :end-before: tag:links-end From 51afe06ff7a51777408489305d85260700a3ed59 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 24 Aug 2016 16:18:23 -0700 Subject: [PATCH 125/331] in progress change --- certbot/auth_handler.py | 12 +++++++- certbot/cli.py | 40 +++++++++++++++++++++++++ certbot/client.py | 2 +- certbot/plugins/standalone.py | 47 +++++++++--------------------- certbot/plugins/standalone_test.py | 32 +++++++------------- certbot/tests/auth_handler_test.py | 6 ++-- certbot/tests/cli_test.py | 18 ++++++++++++ 7 files changed, 96 insertions(+), 61 deletions(-) diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index a94734572..dcde3f9a7 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -33,14 +33,16 @@ class AuthHandler(object): and values are :class:`acme.messages.AuthorizationResource` :ivar list achalls: DV challenges in the form of :class:`certbot.achallenges.AnnotatedChallenge` + :ivar list pref_challs: A list of user specified preferred challenges """ - def __init__(self, auth, acme, account): + def __init__(self, auth, acme, account, pref_challs): self.auth = auth self.acme = acme self.account = account self.authzr = dict() + self.pref_challs = pref_challs # List must be used to keep responses straight. self.achalls = [] @@ -246,6 +248,14 @@ class AuthHandler(object): """ # Make sure to make a copy... chall_prefs = [] + plugin_pref = self.auth.get_chall_pref(domain) + if self.pref_challs: + out = [pref for pref in self.pref_challs if pref in plugin_pref] + if out: + return out + else: + raise errors.AuthorizationError( + "None of the selected challenges are supported by the selected plugins") chall_prefs.extend(self.auth.get_chall_pref(domain)) return chall_prefs diff --git a/certbot/cli.py b/certbot/cli.py index 46ff74cd0..a0cd9b173 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -13,6 +13,8 @@ import six import certbot +from acme import challenges + from certbot import constants from certbot import crypto_util from certbot import errors @@ -844,6 +846,13 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") + helpful.add( + "security", "--preferred-challenges", dest="pref_chall", + action=_PrefChallAction, default=[], + help="Specify which challenges you'd prefer to use. If any of those " + "challenges are valid for your authenticator they will be used. " + "Otherwise Certbot will not attempt authorization. The first " + "challenge listed that is supported by the plugin will be used.") helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." @@ -1032,3 +1041,34 @@ def add_domains(args_or_config, domains): args_or_config.domains.append(domain) return validated_domains + +class _PrefChallAction(argparse.Action): + """Action class for parsing preferred challenges.""" + + def __call__(self, parser, namespace, pref_chall, option_string=None): + """Just wrap add_pref_challs in argparseese.""" + _ = add_pref_challs(namespace, pref_chall) + +def add_pref_challs(namespace, pref_challs): + """Parses and validates user specified challenge types. + + Adds challenges (in order) to the configuration object. + + :param namespace: parsed command line arguments + :type namespace: argparse.Namespace or + configuration.NamespaceConfig + :param str pref_challs: one or more comma separated challenge types + + :returns: Challenge objects which match the validated string inputs + :rtype: `list` + """ + challs = pref_challs.split(",") + unrecognized = [name for name in challs if name not in challenges.Challenge.TYPES] + if unrecognized: + raise argparse.ArgumentTypeError( + "Unrecognized challenges: {0}".format(", ".join(unrecognized))) + + out = [challenges.Challenge.TYPES[name] for name in challs] + print(namespace) + namespace.pref_chall.extend(out) + return out diff --git a/certbot/client.py b/certbot/client.py index 119fb0947..d80a67bad 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -186,7 +186,7 @@ class Client(object): if auth is not None: self.auth_handler = auth_handler.AuthHandler( - auth, self.acme, self.account) + auth, self.acme, self.account, self.config.pref_chall) else: self.auth_handler = None diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 97aca351a..1fe79e8dd 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -3,6 +3,7 @@ import argparse import collections import logging import socket +import sys import threading import OpenSSL @@ -12,6 +13,7 @@ import zope.interface from acme import challenges from acme import standalone as acme_standalone +from certbot import cli from certbot import errors from certbot import interfaces @@ -110,38 +112,17 @@ class ServerManager(object): in six.iteritems(self._instances)) -SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01] -def supported_challenges_validator(data): - """Supported challenges validator for the `argparse`. - - It should be passed as `type` argument to `add_argument`. - - """ - challs = data.split(",") - - # tls-sni-01 was dvsni during private beta - if "dvsni" in challs: - logger.info("Updating legacy standalone_supported_challenges value") - challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall - for chall in challs] - data = ",".join(challs) - - unrecognized = [name for name in challs - if name not in challenges.Challenge.TYPES] - if unrecognized: - raise argparse.ArgumentTypeError( - "Unrecognized challenges: {0}".format(", ".join(unrecognized))) - - choices = set(chall.typ for chall in SUPPORTED_CHALLENGES) - if not set(challs).issubset(choices): - raise argparse.ArgumentTypeError( - "Plugin does not support the following (valid) " - "challenges: {0}".format(", ".join(set(challs) - choices))) - - return data +class supported_challenges_wrapper(argparse.Action): + """Wrapper for the depricated supported challenges flag""" + def __call__(self, parser, namespace, pref_chall, option_string=None): + # print deprecation warning + sys.stderr.write("WARNING: The standalone specific supported challenges flag is depricated") + sys.stderr.write("\nPlease use the --preferred-challenges flag instead.\n") + #call cli version - move namespace back into it + _ = cli.add_pref_challs(namespace, pref_chall) @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) @@ -178,14 +159,12 @@ class Authenticator(common.Plugin): def add_parser_arguments(cls, add): add("supported-challenges", help="Supported challenges. Preferred in the order they are listed.", - type=supported_challenges_validator, - default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES)) + action=supported_challenges_wrapper, dest="pref_chall") @property def supported_challenges(self): """Challenges supported by this plugin.""" - return [challenges.Challenge.TYPES[name] for name in - self.conf("supported-challenges").split(",")] + return self.config.pref_chall @property def _necessary_ports(self): @@ -208,7 +187,7 @@ class Authenticator(common.Plugin): def get_chall_pref(self, domain): # pylint: disable=unused-argument,missing-docstring - return self.supported_challenges + return [challenges.TLSSNI01, challenges.HTTP01] def perform(self, achalls): # pylint: disable=missing-docstring renewer = self.config.verb == "renew" diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index eb6631732..b77ec0554 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -67,29 +67,17 @@ class ServerManagerTest(unittest.TestCase): class SupportedChallengesValidatorTest(unittest.TestCase): """Tests for plugins.standalone.supported_challenges_validator.""" - def _call(self, data): - from certbot.plugins.standalone import ( - supported_challenges_validator) - return supported_challenges_validator(data) - - def test_correct(self): - self.assertEqual("tls-sni-01", self._call("tls-sni-01")) - self.assertEqual("http-01", self._call("http-01")) - self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) - self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) - - def test_unrecognized(self): - assert "foo" not in challenges.Challenge.TYPES - self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") - - def test_not_subset(self): - self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") - - def test_dvsni(self): - self.assertEqual("tls-sni-01", self._call("dvsni")) - self.assertEqual("http-01,tls-sni-01", self._call("http-01,dvsni")) - self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) + def setUp(self): + self.parser = argparse.ArgumentParser() + from certbot.plugins import standalone + standalone.Authenticator.add_parser_arguments(self.parser.add_argument) + def test_standalone_flag(self): + config = self.parser.parse_args(["--supported_challenges", "http-01"]) + http = challenges.Challenge.TYPES["http-01"] + tls = challenges.Challenge.TYPES["tls-sni-01"] + print config + self.assertEqual(config.pref_chall, [tls, http]) class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.standalone.Authenticator.""" diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index fce130f7c..e6e2445d9 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -24,7 +24,7 @@ class ChallengeFactoryTest(unittest.TestCase): from certbot.auth_handler import AuthHandler # Account is mocked... - self.handler = AuthHandler(None, None, mock.Mock(key="mock_key")) + self.handler = AuthHandler(None, None, mock.Mock(key="mock_key"), []) self.dom = "test" self.handler.authzr[self.dom] = acme_util.gen_authzr( @@ -74,7 +74,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_net = mock.MagicMock(spec=acme_client.Client) self.handler = AuthHandler( - self.mock_auth, self.mock_net, self.mock_account) + self.mock_auth, self.mock_net, self.mock_account, []) logging.disable(logging.CRITICAL) @@ -189,7 +189,7 @@ class PollChallengesTest(unittest.TestCase): # Account and network are mocked... self.mock_net = mock.MagicMock() self.handler = AuthHandler( - None, self.mock_net, mock.Mock(key="mock_key")) + None, self.mock_net, mock.Mock(key="mock_key"), []) self.doms = ["0", "1", "2"] self.handler.authzr[self.doms[0]] = acme_util.gen_authzr( diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 2c6e32705..3fe330b96 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -1035,6 +1035,24 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods namespace = parse(short_args) self.assertTrue(namespace.text_mode) + #TODO massage this to work in cli + def test_correct(self): + self.assertEqual("tls-sni-01", self._call("tls-sni-01")) + self.assertEqual("http-01", self._call("http-01")) + self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) + + def test_unrecognized(self): + assert "foo" not in challenges.Challenge.TYPES + self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") + + def test_not_subset(self): + self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") + + def test_dvsni(self): + self.assertEqual("tls-sni-01", self._call("dvsni")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,dvsni")) + self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" From d3bdf9772070d733c313c451215193567ce4e2c7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Aug 2016 16:39:41 -0700 Subject: [PATCH 126/331] fix standalone flag test --- certbot/plugins/standalone.py | 2 +- certbot/plugins/standalone_test.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 1fe79e8dd..b239029ba 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -159,7 +159,7 @@ class Authenticator(common.Plugin): def add_parser_arguments(cls, add): add("supported-challenges", help="Supported challenges. Preferred in the order they are listed.", - action=supported_challenges_wrapper, dest="pref_chall") + action=supported_challenges_wrapper, default= [], dest="pref_chall") @property def supported_challenges(self): diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index b77ec0554..24b28c6eb 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -14,6 +14,8 @@ from certbot import achallenges from certbot import errors from certbot import interfaces +from certbot.plugins import disco + from certbot.tests import acme_util from certbot.tests import test_util @@ -69,14 +71,15 @@ class SupportedChallengesValidatorTest(unittest.TestCase): def setUp(self): self.parser = argparse.ArgumentParser() - from certbot.plugins import standalone - standalone.Authenticator.add_parser_arguments(self.parser.add_argument) + name = "standalone" + disco.PluginsRegistry.find_all()[name].plugin_cls.inject_parser_options( + self.parser, name) def test_standalone_flag(self): - config = self.parser.parse_args(["--supported_challenges", "http-01"]) + config = self.parser.parse_args(["--standalone-supported-challenges", + "tls-sni-01,http-01"]) http = challenges.Challenge.TYPES["http-01"] tls = challenges.Challenge.TYPES["tls-sni-01"] - print config self.assertEqual(config.pref_chall, [tls, http]) class AuthenticatorTest(unittest.TestCase): From 5b112cef7a6d52610d0ae5462cb5011e581bff50 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 25 Aug 2016 16:58:23 -0700 Subject: [PATCH 127/331] revert standalone --- certbot/plugins/standalone.py | 47 +++++++++++++++++++++--------- certbot/plugins/standalone_test.py | 35 +++++++++++++--------- certbot/tests/cli_test.py | 17 ----------- 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index b239029ba..97aca351a 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -3,7 +3,6 @@ import argparse import collections import logging import socket -import sys import threading import OpenSSL @@ -13,7 +12,6 @@ import zope.interface from acme import challenges from acme import standalone as acme_standalone -from certbot import cli from certbot import errors from certbot import interfaces @@ -112,17 +110,38 @@ class ServerManager(object): in six.iteritems(self._instances)) +SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01] -class supported_challenges_wrapper(argparse.Action): - """Wrapper for the depricated supported challenges flag""" +def supported_challenges_validator(data): + """Supported challenges validator for the `argparse`. + + It should be passed as `type` argument to `add_argument`. + + """ + challs = data.split(",") + + # tls-sni-01 was dvsni during private beta + if "dvsni" in challs: + logger.info("Updating legacy standalone_supported_challenges value") + challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall + for chall in challs] + data = ",".join(challs) + + unrecognized = [name for name in challs + if name not in challenges.Challenge.TYPES] + if unrecognized: + raise argparse.ArgumentTypeError( + "Unrecognized challenges: {0}".format(", ".join(unrecognized))) + + choices = set(chall.typ for chall in SUPPORTED_CHALLENGES) + if not set(challs).issubset(choices): + raise argparse.ArgumentTypeError( + "Plugin does not support the following (valid) " + "challenges: {0}".format(", ".join(set(challs) - choices))) + + return data - def __call__(self, parser, namespace, pref_chall, option_string=None): - # print deprecation warning - sys.stderr.write("WARNING: The standalone specific supported challenges flag is depricated") - sys.stderr.write("\nPlease use the --preferred-challenges flag instead.\n") - #call cli version - move namespace back into it - _ = cli.add_pref_challs(namespace, pref_chall) @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) @@ -159,12 +178,14 @@ class Authenticator(common.Plugin): def add_parser_arguments(cls, add): add("supported-challenges", help="Supported challenges. Preferred in the order they are listed.", - action=supported_challenges_wrapper, default= [], dest="pref_chall") + type=supported_challenges_validator, + default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES)) @property def supported_challenges(self): """Challenges supported by this plugin.""" - return self.config.pref_chall + return [challenges.Challenge.TYPES[name] for name in + self.conf("supported-challenges").split(",")] @property def _necessary_ports(self): @@ -187,7 +208,7 @@ class Authenticator(common.Plugin): def get_chall_pref(self, domain): # pylint: disable=unused-argument,missing-docstring - return [challenges.TLSSNI01, challenges.HTTP01] + return self.supported_challenges def perform(self, achalls): # pylint: disable=missing-docstring renewer = self.config.verb == "renew" diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 24b28c6eb..eb6631732 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -14,8 +14,6 @@ from certbot import achallenges from certbot import errors from certbot import interfaces -from certbot.plugins import disco - from certbot.tests import acme_util from certbot.tests import test_util @@ -69,18 +67,29 @@ class ServerManagerTest(unittest.TestCase): class SupportedChallengesValidatorTest(unittest.TestCase): """Tests for plugins.standalone.supported_challenges_validator.""" - def setUp(self): - self.parser = argparse.ArgumentParser() - name = "standalone" - disco.PluginsRegistry.find_all()[name].plugin_cls.inject_parser_options( - self.parser, name) + def _call(self, data): + from certbot.plugins.standalone import ( + supported_challenges_validator) + return supported_challenges_validator(data) + + def test_correct(self): + self.assertEqual("tls-sni-01", self._call("tls-sni-01")) + self.assertEqual("http-01", self._call("http-01")) + self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) + + def test_unrecognized(self): + assert "foo" not in challenges.Challenge.TYPES + self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") + + def test_not_subset(self): + self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") + + def test_dvsni(self): + self.assertEqual("tls-sni-01", self._call("dvsni")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,dvsni")) + self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) - def test_standalone_flag(self): - config = self.parser.parse_args(["--standalone-supported-challenges", - "tls-sni-01,http-01"]) - http = challenges.Challenge.TYPES["http-01"] - tls = challenges.Challenge.TYPES["tls-sni-01"] - self.assertEqual(config.pref_chall, [tls, http]) class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.standalone.Authenticator.""" diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 3fe330b96..5a8845c4c 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -1036,23 +1036,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue(namespace.text_mode) #TODO massage this to work in cli - def test_correct(self): - self.assertEqual("tls-sni-01", self._call("tls-sni-01")) - self.assertEqual("http-01", self._call("http-01")) - self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) - self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) - - def test_unrecognized(self): - assert "foo" not in challenges.Challenge.TYPES - self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") - - def test_not_subset(self): - self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") - - def test_dvsni(self): - self.assertEqual("tls-sni-01", self._call("dvsni")) - self.assertEqual("http-01,tls-sni-01", self._call("http-01,dvsni")) - self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" From d8031f16bd85aa4d2f1a04edf1b816a3bd5c2d47 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 25 Aug 2016 17:45:13 -0700 Subject: [PATCH 128/331] add deprication --- certbot/plugins/standalone.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 97aca351a..c00f30052 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -3,6 +3,7 @@ import argparse import collections import logging import socket +import sys import threading import OpenSSL @@ -119,6 +120,8 @@ def supported_challenges_validator(data): It should be passed as `type` argument to `add_argument`. """ + sys.stderr.write("WARNING: The standalone specific supported challenges flag is depricated") + sys.stderr.write("\nPlease use the --preferred-challenges flag instead.\n") challs = data.split(",") # tls-sni-01 was dvsni during private beta @@ -177,7 +180,7 @@ class Authenticator(common.Plugin): @classmethod def add_parser_arguments(cls, add): add("supported-challenges", - help="Supported challenges. Preferred in the order they are listed.", + help=argparse.SUPPRESS, type=supported_challenges_validator, default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES)) From 4814f043303a566d36e1368ce30b20bc8bc40458 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 26 Aug 2016 13:11:45 -0700 Subject: [PATCH 129/331] remove old todo --- certbot/tests/cli_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 5a8845c4c..d011be957 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -1035,8 +1035,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods namespace = parse(short_args) self.assertTrue(namespace.text_mode) - #TODO massage this to work in cli - class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" From 5115e6ac2f47a81fad9226bef6b4ddd4e8e41ad9 Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Fri, 26 Aug 2016 16:17:19 -0700 Subject: [PATCH 130/331] Support both invalidEmail and invalidContact errors --- acme/acme/messages.py | 2 ++ certbot/client.py | 2 +- certbot/tests/client_test.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 56bcb1de2..940d1efc0 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -27,6 +27,8 @@ class Error(jose.JSONObjectWithFields, errors.Error): ('dnssec', 'The server could not validate a DNSSEC signed domain'), ('invalidEmail', 'The provided email for a registration was invalid'), + ('invalidContact', + 'The provided email for a registration was invalid'), ('malformed', 'The request message was malformed'), ('rateLimited', 'There were too many requests of a given type'), ('serverInternal', 'The server experienced an internal error'), diff --git a/certbot/client.py b/certbot/client.py index ef59c6ce3..f92370d30 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -149,7 +149,7 @@ def perform_registration(acme, config): try: return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error as e: - if e.typ == "urn:acme:error:invalidEmail": + if e.typ == "urn:acme:error:invalidEmail" or e.typ == "urn:acme:error:invalidContact": if config.noninteractive_mode: msg = ("The ACME server believes %s is an invalid email address. " "Please ensure it is a valid email and attempt " diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 98d853716..e5c6b3af9 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -65,7 +65,7 @@ class RegisterTest(unittest.TestCase): from acme import messages self.config.noninteractive_mode = False msg = "DNS problem: NXDOMAIN looking up MX for example.com" - mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidEmail") + mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidContact") with mock.patch("certbot.client.acme_client.Client") as mock_client: mock_client().register.side_effect = [mx_err, mock.MagicMock()] self._call() @@ -75,7 +75,7 @@ class RegisterTest(unittest.TestCase): def test_email_invalid_noninteractive(self, _rep): from acme import messages msg = "DNS problem: NXDOMAIN looking up MX for example.com" - mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidEmail") + mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidContact") with mock.patch("certbot.client.acme_client.Client") as mock_client: mock_client().register.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(errors.Error, self._call) From cd5b91e4ae0e323044a7264dcd00e4a70d18615e Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Sat, 27 Aug 2016 19:14:42 +0200 Subject: [PATCH 131/331] Adding root certbot-auto to modification check --- tests/modification-check.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/modification-check.sh b/tests/modification-check.sh index b9cc669ff..53a5efa93 100755 --- a/tests/modification-check.sh +++ b/tests/modification-check.sh @@ -3,19 +3,33 @@ temp_dir=`mktemp -d` # Script should be run from Certbot's root directory -cp letsencrypt-auto ${temp_dir}/to-be-checked +cp letsencrypt-auto ${temp_dir}/letsencrypt-to-be-checked +cp certbot-auto ${temp_dir}/certbot-to-be-checked + cp letsencrypt-auto-source/pieces/fetch.py ${temp_dir}/fetch.py cd ${temp_dir} LATEST_VERSION=`python fetch.py --latest-version` python fetch.py --le-auto-script v${LATEST_VERSION} -cmp -s letsencrypt-auto to-be-checked +cmp -s letsencrypt-auto letsencrypt-to-be-checked if [ $? != 0 ]; then echo "Root letsencrypt-auto has changed." rm -rf temp_dir exit 1 +else + echo "Root letsencrypt-auto is unchanged." +fi + +cmp -s letsencrypt-auto certbot-to-be-checked + +if [ $? != 0 ]; then + echo "Root certbot-auto has changed." + rm -rf temp_dir + exit 1 +else + echo "Root certbot-auto is unchanged." fi rm -rf temp_dir From f1ff5516d1f1ae70b79579375b33f73acc4907ef Mon Sep 17 00:00:00 2001 From: Gordin <9ordin@gmail.com> Date: Sun, 28 Aug 2016 20:29:22 +0200 Subject: [PATCH 132/331] Fixed hash_bucket_size detection for nginx --- certbot-nginx/certbot_nginx/tls_sni_01.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index 40382cab0..0543000ea 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -101,7 +101,7 @@ class NginxTlsSni01(common.TLSSNI01): if key == ['http']: found_bucket = False for k, _ in body: - if k == bucket_directive[0]: + if k == bucket_directive[1]: found_bucket = True if not found_bucket: body.insert(0, bucket_directive) From 17d54a5f6ada2ad8b43f3e2d540ce8f199cdd24c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 11:59:07 -0700 Subject: [PATCH 133/331] make docstring more explicit --- certbot/auth_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index dcde3f9a7..e9b4d66c8 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -33,7 +33,9 @@ class AuthHandler(object): and values are :class:`acme.messages.AuthorizationResource` :ivar list achalls: DV challenges in the form of :class:`certbot.achallenges.AnnotatedChallenge` - :ivar list pref_challs: A list of user specified preferred challenges + :ivar list pref_challs: sorted user specified preferred challenges + in the form of subclasses of :class:`acme.challenges.Challenge` + with the most preferred challenge listed first """ def __init__(self, auth, acme, account, pref_challs): From 349c2c591564a506ee7d2c169066067fd19cad04 Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Mon, 29 Aug 2016 12:04:27 -0700 Subject: [PATCH 134/331] Switch out error message --- acme/acme/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 940d1efc0..563f80627 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -28,7 +28,7 @@ class Error(jose.JSONObjectWithFields, errors.Error): ('invalidEmail', 'The provided email for a registration was invalid'), ('invalidContact', - 'The provided email for a registration was invalid'), + 'The provided contact URI was invalid'), ('malformed', 'The request message was malformed'), ('rateLimited', 'There were too many requests of a given type'), ('serverInternal', 'The server experienced an internal error'), From d4f81f825c72e6930e72c9e648612daa3dcc0ebc Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 12:18:57 -0700 Subject: [PATCH 135/331] minor _get_chall_pref cleanup --- certbot/auth_handler.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index e9b4d66c8..cc8beb463 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -248,17 +248,18 @@ class AuthHandler(object): :param str domain: domain for which you are requesting preferences """ - # Make sure to make a copy... chall_prefs = [] + # Make sure to make a copy... plugin_pref = self.auth.get_chall_pref(domain) if self.pref_challs: - out = [pref for pref in self.pref_challs if pref in plugin_pref] - if out: - return out - else: - raise errors.AuthorizationError( - "None of the selected challenges are supported by the selected plugins") - chall_prefs.extend(self.auth.get_chall_pref(domain)) + chall_prefs.extend(pref for pref in self.pref_challs + if pref in plugin_pref) + if chall_prefs: + return chall_prefs + raise errors.AuthorizationError( + "None of the preferred challenges " + "are supported by the selected plugin") + chall_prefs.extend(plugin_pref) return chall_prefs def _cleanup_challenges(self, achall_list=None): From 1560fd46805571032e0ba77814121423bd27b7d3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 12:41:55 -0700 Subject: [PATCH 136/331] cleanup pref_challs help, use consistent naming, and simplify parsing --- certbot/cli.py | 53 ++++++++++++++++------------------------------- certbot/client.py | 2 +- 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index a0cd9b173..c236041ce 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -11,10 +11,10 @@ import sys import configargparse import six -import certbot - from acme import challenges +import certbot + from certbot import constants from certbot import crypto_util from certbot import errors @@ -847,12 +847,13 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") helpful.add( - "security", "--preferred-challenges", dest="pref_chall", - action=_PrefChallAction, default=[], - help="Specify which challenges you'd prefer to use. If any of those " - "challenges are valid for your authenticator they will be used. " - "Otherwise Certbot will not attempt authorization. The first " - "challenge listed that is supported by the plugin will be used.") + ["certonly", "renew", "run"], "--preferred-challenges", + dest="pref_challs", action=_PrefChallAction, default=[], + help="A sorted, comma delimited list of the preferred challenge to " + "use during authorization with the most preferred challenge " + "listed first (e.g. tls-sni-01,http-01). If none of the " + "preferred challenges can be used by the selected plugin to " + "satisfy the CA, authorization is not attempted.") helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." @@ -1045,30 +1046,12 @@ def add_domains(args_or_config, domains): class _PrefChallAction(argparse.Action): """Action class for parsing preferred challenges.""" - def __call__(self, parser, namespace, pref_chall, option_string=None): - """Just wrap add_pref_challs in argparseese.""" - _ = add_pref_challs(namespace, pref_chall) - -def add_pref_challs(namespace, pref_challs): - """Parses and validates user specified challenge types. - - Adds challenges (in order) to the configuration object. - - :param namespace: parsed command line arguments - :type namespace: argparse.Namespace or - configuration.NamespaceConfig - :param str pref_challs: one or more comma separated challenge types - - :returns: Challenge objects which match the validated string inputs - :rtype: `list` - """ - challs = pref_challs.split(",") - unrecognized = [name for name in challs if name not in challenges.Challenge.TYPES] - if unrecognized: - raise argparse.ArgumentTypeError( - "Unrecognized challenges: {0}".format(", ".join(unrecognized))) - - out = [challenges.Challenge.TYPES[name] for name in challs] - print(namespace) - namespace.pref_chall.extend(out) - return out + def __call__(self, parser, namespace, pref_challs, option_string=None): + challs = pref_challs.split(",") + unrecognized = ", ".join(name for name in challs + if name not in challenges.Challenge.TYPES) + if unrecognized: + raise argparse.ArgumentTypeError( + "Unrecognized challenges: {0}".format(unrecognized)) + namespace.pref_challs.extend(challenges.Challenge.TYPES[name] + for name in challs) diff --git a/certbot/client.py b/certbot/client.py index 66e90bb1f..44eb67e48 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -192,7 +192,7 @@ class Client(object): if auth is not None: self.auth_handler = auth_handler.AuthHandler( - auth, self.acme, self.account, self.config.pref_chall) + auth, self.acme, self.account, self.config.pref_challs) else: self.auth_handler = None From c0d060a8f1e45d23048638851735ed4a394fbebd Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 12:44:20 -0700 Subject: [PATCH 137/331] fix typo and long line --- certbot/plugins/standalone.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index c00f30052..f3cd4b49d 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -120,8 +120,10 @@ def supported_challenges_validator(data): It should be passed as `type` argument to `add_argument`. """ - sys.stderr.write("WARNING: The standalone specific supported challenges flag is depricated") - sys.stderr.write("\nPlease use the --preferred-challenges flag instead.\n") + sys.stderr.write( + "WARNING: The standalone specific " + "supported challenges flag is deprecated\n") + sys.stderr.write("Please use the --preferred-challenges flag instead.\n") challs = data.split(",") # tls-sni-01 was dvsni during private beta From 74677946373d38a7e1720b70c45332d958cde0b3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 12:46:41 -0700 Subject: [PATCH 138/331] readd newline to prevent pep8 errors --- certbot/tests/cli_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index d011be957..2c6e32705 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -1035,6 +1035,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods namespace = parse(short_args) self.assertTrue(namespace.text_mode) + class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" From 24cc03d1f6e3d3fb5a89aa489b0bd2f71fc80684 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 13:32:19 -0700 Subject: [PATCH 139/331] only print deprecation warning when --standalone-supported-challenges is set --- certbot/plugins/standalone.py | 10 ++++++---- certbot/plugins/standalone_test.py | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index f3cd4b49d..0195b2726 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -13,6 +13,7 @@ import zope.interface from acme import challenges from acme import standalone as acme_standalone +from certbot import cli from certbot import errors from certbot import interfaces @@ -120,10 +121,11 @@ def supported_challenges_validator(data): It should be passed as `type` argument to `add_argument`. """ - sys.stderr.write( - "WARNING: The standalone specific " - "supported challenges flag is deprecated\n") - sys.stderr.write("Please use the --preferred-challenges flag instead.\n") + if cli.set_by_cli("standalone_supported_challenges"): + sys.stderr.write( + "WARNING: The standalone specific " + "supported challenges flag is deprecated.\n" + "Please use the --preferred-challenges flag instead.\n") challs = data.split(",") # tls-sni-01 was dvsni during private beta diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index eb6631732..1dfa3950a 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -67,10 +67,25 @@ class ServerManagerTest(unittest.TestCase): class SupportedChallengesValidatorTest(unittest.TestCase): """Tests for plugins.standalone.supported_challenges_validator.""" + def setUp(self): + self.set_by_cli_patch = mock.patch( + "certbot.plugins.standalone.cli.set_by_cli") + self.stderr_patch = mock.patch("certbot.plugins.standalone.sys.stderr") + + self.set_by_cli_patch.start().return_value = True + self.stderr = self.stderr_patch.start() + + def tearDown(self): + self.set_by_cli_patch.stop() + self.stderr_patch.stop() + def _call(self, data): from certbot.plugins.standalone import ( supported_challenges_validator) - return supported_challenges_validator(data) + return_value = supported_challenges_validator(data) + self.assertTrue(self.stderr.write.called) # pylint: disable=no-member + self.stderr.write.reset_mock() # pylint: disable=no-member + return return_value def test_correct(self): self.assertEqual("tls-sni-01", self._call("tls-sni-01")) From 606636766e85469bd7268a12f24a39797a4c02aa Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 14:13:52 -0700 Subject: [PATCH 140/331] Test preferred challenges not supported --- certbot/tests/auth_handler_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index e6e2445d9..4e2db2712 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -167,6 +167,13 @@ class GetAuthorizationsTest(unittest.TestCase): def test_no_domains(self): self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, []) + def test_preferred_challenges_not_supported(self): + self.mock_net.request_domain_challenges.side_effect = functools.partial( + gen_dom_authzr, challs=acme_util.CHALLENGES) + self.handler.pref_challs.append(challenges.HTTP01) + self.assertRaises( + errors.AuthorizationError, self.handler.get_authorizations, ["0"]) + def _validate_all(self, unused_1, unused_2): for dom in six.iterkeys(self.handler.authzr): azr = self.handler.authzr[dom] From 0f575cf80d48b66fbbd620364d51a03489e1dc3e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 14:34:03 -0700 Subject: [PATCH 141/331] test that pref_challs is respected --- certbot/tests/auth_handler_test.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 4e2db2712..bb1fbc912 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -167,6 +167,22 @@ class GetAuthorizationsTest(unittest.TestCase): def test_no_domains(self): self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, []) + @mock.patch("certbot.auth_handler.AuthHandler._poll_challenges") + def test_preferred_challenge_choice(self, mock_poll): + self.mock_net.request_domain_challenges.side_effect = functools.partial( + gen_dom_authzr, challs=acme_util.CHALLENGES) + + mock_poll.side_effect = self._validate_all + self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) + + self.handler.pref_challs.extend((challenges.HTTP01, challenges.DNS,)) + + self.handler.get_authorizations(["0"]) + + self.assertEqual(self.mock_auth.cleanup.call_count, 1) + self.assertEqual( + self.mock_auth.cleanup.call_args[0][0][0].typ, "http-01") + def test_preferred_challenges_not_supported(self): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) From 322546b8d132ad2a040606975f49b1d80e2c9f1b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 15:44:31 -0700 Subject: [PATCH 142/331] make manual use --preferred-challenges flag --- certbot/plugins/manual.py | 41 ++++++++++++++++------------------ certbot/plugins/manual_test.py | 6 ++--- certbot/plugins/util.py | 36 ----------------------------- certbot/plugins/util_test.py | 38 ------------------------------- certbot/util.py | 21 ----------------- 5 files changed, 22 insertions(+), 120 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index a7ba571d7..6fe8cd597 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -4,31 +4,27 @@ import logging import pipes import shutil import signal +import socket import subprocess import sys import tempfile +import time import six import zope.component import zope.interface -from functools import partial - from acme import challenges from acme import errors as acme_errors from certbot import errors from certbot import interfaces -from certbot.plugins import common, util -from certbot.util import busy_wait +from certbot.plugins import common logger = logging.getLogger(__name__) -SUPPORTED_CHALLENGES = [challenges.HTTP01, challenges.DNS01] - - @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): @@ -99,23 +95,10 @@ s.serve_forever()" """ @classmethod def add_parser_arguments(cls, add): - validator = partial(util.supported_challenges_validator, - supported=SUPPORTED_CHALLENGES) - add("test-mode", action="store_true", help="Test mode. Executes the manual command in subprocess.") add("public-ip-logging-ok", action="store_true", help="Automatically allows public IP logging.") - add("supported-challenges", - help="Supported challenges. Preferred in the order they are listed.", - type=validator, - default="http-01") - - @property - def supported_challenges(self): - """Challenges supported by this plugin.""" - return [challenges.Challenge.TYPES[name] for name in - self.conf("supported-challenges").split(",")] def prepare(self): # pylint: disable=missing-docstring,no-self-use if self.config.noninteractive_mode and not self.conf("test-mode"): @@ -132,7 +115,7 @@ s.serve_forever()" """ def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument - return self.supported_challenges + return [challenges.HTTP01, challenges.DNS01] def perform(self, achalls): # pylint: disable=missing-docstring @@ -145,6 +128,20 @@ s.serve_forever()" """ responses.append(mapping[achall.typ](achall)) return responses + @classmethod + def _test_mode_busy_wait(cls, port): + while True: + time.sleep(1) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect(("localhost", port)) + except socket.error: # pragma: no cover + pass + else: + break + finally: + sock.close() + def cleanup(self, achalls): # pylint: disable=missing-docstring for achall in achalls: @@ -184,7 +181,7 @@ s.serve_forever()" """ logger.debug("Manual command running as PID %s.", self._httpd.pid) # give it some time to bootstrap, before we try to verify # (cert generation in case of simpleHttpS might take time) - busy_wait(port) + self._test_mode_busy_wait(port) if self._httpd.poll() is not None: raise errors.Error("Couldn't execute manual command") diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index 0179c2932..2fb679a36 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -32,7 +32,7 @@ class AuthenticatorTest(unittest.TestCase): self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY) self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY) + challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY) self.achalls = [self.http01, self.dns01] @@ -106,8 +106,8 @@ class AuthenticatorTest(unittest.TestCase): mock_popen.side_effect = OSError self.assertEqual([False], self.auth_test_mode.perform([self.http01])) - @mock.patch("certbot.util.socket.socket") - @mock.patch("certbot.util.time.sleep", autospec=True) + @mock.patch("certbot.plugins.manual.socket.socket") + @mock.patch("certbot.plugins.manual.time.sleep", autospec=True) @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_command_run_failure( self, mock_popen, unused_mock_sleep, unused_mock_socket): diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index 5a8789c79..b97ca1afd 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -1,13 +1,10 @@ """Plugin utilities.""" -import argparse import logging import os import socket import zope.component -from acme import challenges - from certbot import interfaces from certbot import util @@ -157,36 +154,3 @@ def already_listening_psutil(port, renewer=False): # name (AccessDenied). pass return False - - -def supported_challenges_validator(data, supported=None): - """Supported challenges validator for the `argparse`. - - It should be passed as `type` argument to `add_argument`. - - :param str data: input value representing the supported_challenges - :returns: original value if valid - """ - challs = data.split(",") - supported = supported or [] - - # tls-sni-01 was dvsni during private beta - if "dvsni" in challs: - logger.info("Updating legacy standalone_supported_challenges value") - challs = [challenges.TLSSNI01.typ if chall == "dvsni" else chall - for chall in challs] - data = ",".join(challs) - - unrecognized = [name for name in challs - if name not in challenges.Challenge.TYPES] - if unrecognized: - raise argparse.ArgumentTypeError( - "Unrecognized challenges: {0}".format(", ".join(unrecognized))) - - choices = set(chall.typ for chall in supported) - if not set(challs).issubset(choices): - raise argparse.ArgumentTypeError( - "Plugin does not support the following (valid) " - "challenges: {0}".format(", ".join(set(challs) - choices))) - - return data diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index 200c3d9c5..27ede6533 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -1,5 +1,4 @@ """Tests for certbot.plugins.util.""" -import argparse import os import unittest import sys @@ -212,42 +211,5 @@ class AlreadyListeningTestPsutil(unittest.TestCase): mock_net.side_effect = psutil.AccessDenied("") self.assertFalse(self._call(12345)) - -class SupportedChallengesValidatorTest(unittest.TestCase): - """Tests for plugins.standalone.supported_challenges_validator.""" - - def _call(self, data): - from certbot.plugins.util import supported_challenges_validator - from acme import challenges - - supported = [challenges.HTTP01, challenges.DNS01, challenges.TLSSNI01] - - return supported_challenges_validator(data, supported=supported) - - def test_correct(self): - self.assertEqual("tls-sni-01", self._call("tls-sni-01")) - self.assertEqual("http-01", self._call("http-01")) - self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) - self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) - - def test_unrecognized(self): - from acme import challenges - - assert "foo" not in challenges.Challenge.TYPES - self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") - - def test_not_subset(self): - self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") - - def test_dvsni(self): - self.assertEqual("tls-sni-01", self._call("dvsni")) - self.assertEqual("http-01,tls-sni-01", self._call("http-01,dvsni")) - self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) - - def test_dns01(self): - self.assertEqual("dns-01", self._call("dns-01")) - self.assertEqual("http-01,dns-01", self._call("http-01,dns-01")) - self.assertEqual("dns-01,http-01", self._call("dns-01,http-01")) - if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/util.py b/certbot/util.py index e5d671ce1..e78ae664c 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -14,7 +14,6 @@ import socket import stat import subprocess import sys -import time import configargparse @@ -475,23 +474,3 @@ def get_strict_version(normalized): # strict version ending with "a" and a number designates a pre-release # pylint: disable=no-member return distutils.version.StrictVersion(normalized.replace(".dev", "a")) - - -def busy_wait(port, host="localhost"): - """Artificialy wait a fixed amount of time on a specific host and port - - :param str port: port of the connection - :param str host: hostname of the connection, "localhost" if None - - """ - while True: - time.sleep(1) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect((host, port)) - except socket.error: # pragma: no cover - pass - else: - break - finally: - sock.close() From b73bdcd51831f91b500dc392a724d51d53321c37 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 15:48:17 -0700 Subject: [PATCH 143/331] Use DNS01 not DNS --- certbot/tests/auth_handler_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index b37e7cd51..84c3e16fa 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -175,7 +175,7 @@ class GetAuthorizationsTest(unittest.TestCase): mock_poll.side_effect = self._validate_all self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) - self.handler.pref_challs.extend((challenges.HTTP01, challenges.DNS,)) + self.handler.pref_challs.extend((challenges.HTTP01, challenges.DNS01,)) self.handler.get_authorizations(["0"]) From 26467d4233307f6999b371297cbee8e74a53badf Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 15:57:03 -0700 Subject: [PATCH 144/331] update manual plugin info --- certbot/plugins/manual.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 6fe8cd597..eeefe20e5 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -106,12 +106,14 @@ s.serve_forever()" """ def more_info(self): # pylint: disable=missing-docstring,no-self-use return ("This plugin requires user's manual intervention in setting " - "up an HTTP server when solving http-01 challenges and thus " - "does not need to be run as a privileged process. " - "Alternatively shows instructions on how to use Python's " - "built-in HTTP server." - "When solving dns-01 challenges, it simply needs to wait for " - "the proper configuration of the domain's dns") + "up challenges to prove control of a domain and does not need " + "to be run as a privileged process. When solving " + "http-01 challenges, the user is responsible for setting up " + "an HTTP server. Alternatively, instructions are shown on how " + "to use Python's built-in HTTP server. The user is " + "responsible for configuration of a domain's DNS when solving " + "dns-01 challenges. The type of challenges used can be " + "controlled through the --preferred-challenges flag.") def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument From b854d10795ee24cd8b3a14175616b1397093ff58 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 16:18:23 -0700 Subject: [PATCH 145/331] reduce number of ip_logging_permission checks --- certbot/plugins/manual.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index eeefe20e5..ae263e0a3 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -121,6 +121,7 @@ s.serve_forever()" """ def perform(self, achalls): # pylint: disable=missing-docstring + self._get_ip_logging_permission() mapping = {"http-01": self._perform_http01_challenge, "dns-01": self._perform_dns01_challenge} responses = [] @@ -188,14 +189,12 @@ s.serve_forever()" """ if self._httpd.poll() is not None: raise errors.Error("Couldn't execute manual command") else: - message = self._get_message(achall) - uri = achall.chall.uri(achall.domain) - formated_message = message.format(validation=validation, - response=response, - uri=uri, - command=command) - - self._ip_logging_permission(formated_message) + self._notify_and_wait( + self._get_message(achall).format( + validation=validation, + response=response, + uri=achall.chall.uri(achall.domain), + command=command)) if not response.simple_verify( achall.chall, achall.domain, @@ -207,11 +206,11 @@ s.serve_forever()" """ def _perform_dns01_challenge(self, achall): response, validation = achall.response_and_validation() if not self.conf("test-mode"): - message = self._get_message(achall) - formated_message = message.format(validation=validation, - domain=achall.domain, - response=response) - self._ip_logging_permission(formated_message) + self._notify_and_wait( + self._get_message(achall).format( + validation=validation, + domain=achall.domain, + response=response)) try: verification_status = response.simple_verify( @@ -247,7 +246,7 @@ s.serve_forever()" """ sys.stdout.write(message) six.moves.input("Press ENTER to continue") - def _ip_logging_permission(self, formated_message): + def _get_ip_logging_permission(self): # pylint: disable=missing-docstring if not self.conf("public-ip-logging-ok"): if not zope.component.getUtility(interfaces.IDisplay).yesno( @@ -257,8 +256,6 @@ s.serve_forever()" """ else: self.config.namespace.manual_public_ip_logging_ok = True - self._notify_and_wait(formated_message) - def _get_message(self, achall): # pylint: disable=missing-docstring,no-self-use,unused-argument return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "") From 55e86983e793e8510b013809cfdb8dd257111928 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 16:30:17 -0700 Subject: [PATCH 146/331] fix test mock after moving getUtility call --- certbot/plugins/manual_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index 2fb679a36..7626f607d 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -52,7 +52,9 @@ class AuthenticatorTest(unittest.TestCase): self.assertTrue(all(issubclass(pref, challenges.Challenge) for pref in self.auth.get_chall_pref("foo.com"))) - def test_perform_empty(self): + @mock.patch("certbot.plugins.manual.zope.component.getUtility") + def test_perform_empty(self, mock_interaction): + mock_interaction().yesno.return_value = True self.assertEqual([], self.auth.perform([])) @mock.patch("certbot.plugins.manual.zope.component.getUtility") From 456f3527cd6d6330c658f320d17840618d481c66 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 16:31:58 -0700 Subject: [PATCH 147/331] Only log one warning when missing dnspython --- certbot/plugins/manual.py | 10 +++++----- certbot/plugins/manual_test.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index ae263e0a3..7ae45a8b5 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -217,11 +217,11 @@ s.serve_forever()" """ achall.chall, achall.domain, achall.account_key.public_key()) except acme_errors.DependencyError: - verification_status = False - logger.warning("Dns challenge requires `dnspython`") - - if not verification_status: - logger.warning("Self-verify of challenge failed.") + logger.warning("Self verification requires optional " + "dependency `dnspython` to be installed.") + else: + if not verification_status: + logger.warning("Self-verify of challenge failed.") return response diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index 7626f607d..e55abbeb5 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -90,7 +90,7 @@ class AuthenticatorTest(unittest.TestCase): with mock.patch("certbot.plugins.manual.logger") as mock_logger: self.auth.perform([self.dns01]) - self.assertEqual(2, mock_logger.warning.call_count) + self.assertEqual(1, mock_logger.warning.call_count) mock_raw_input.assert_called_once_with("Press ENTER to continue") From eb88e7a577de03cd5bc45753ab8f66d0104399ba Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 16:34:43 -0700 Subject: [PATCH 148/331] Remove standalone_supported_challenges value from manual test --- certbot/plugins/manual_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index e55abbeb5..25107e4b4 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -25,8 +25,7 @@ class AuthenticatorTest(unittest.TestCase): from certbot.plugins.manual import Authenticator self.config = mock.MagicMock( http01_port=8080, manual_test_mode=False, - manual_public_ip_logging_ok=False, noninteractive_mode=True, - standalone_supported_challenges="dns-01,http-01") + manual_public_ip_logging_ok=False, noninteractive_mode=True) self.auth = Authenticator(config=self.config, name="manual") self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( From 5d8127177cbcafc72304d2d265bec82811f55e1b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 16:48:31 -0700 Subject: [PATCH 149/331] correct challenge domain name --- certbot/plugins/manual.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 7ae45a8b5..9025dc9cb 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -44,7 +44,8 @@ class Authenticator(common.Plugin): MESSAGE_TEMPLATE = { "dns-01": """\ -To prove control of the domain {domain}, please deploy a DNS TXT record with the following value: +Please deploy a DNS TXT record under the name +{domain} with the following value: {validation} @@ -209,7 +210,7 @@ s.serve_forever()" """ self._notify_and_wait( self._get_message(achall).format( validation=validation, - domain=achall.domain, + domain=achall.validation_domain_name(achall.domain), response=response)) try: From 021313acd12a4449b40c496d6dbe5ebcd33117c0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 17:08:46 -0700 Subject: [PATCH 150/331] make port flags more visible with better help --- certbot/cli.py | 5 +++-- certbot/interfaces.py | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index c236041ce..942fc8ac2 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -790,11 +790,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) helpful.add( - "testing", "--tls-sni-01-port", type=int, + ["certonly", "renew", "run"], "--tls-sni-01-port", type=int, default=flag_default("tls_sni_01_port"), help=config_help("tls_sni_01_port")) helpful.add( - "testing", "--http-01-port", type=int, dest="http01_port", + ["certonly", "renew", "run"], "--http-01-port", type=int, + dest="http01_port", default=flag_default("http01_port"), help=config_help("http01_port")) helpful.add( "testing", "--break-my-certs", action="store_true", diff --git a/certbot/interfaces.py b/certbot/interfaces.py index d4b391378..42a952f10 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -229,11 +229,14 @@ class IConfig(zope.interface.Interface): no_verify_ssl = zope.interface.Attribute( "Disable verification of the ACME server's certificate.") tls_sni_01_port = zope.interface.Attribute( - "Port number to perform tls-sni-01 challenge. " - "Boulder in testing mode defaults to 5001.") + "Port used during tls-sni-01 challenge. " + "This only affects the port Certbot listens on. " + "A conforming ACME server will still attempt to connect on port 443.") http01_port = zope.interface.Attribute( - "Port used in the SimpleHttp challenge.") + "Port used in the http-01 challenge." + "This only affects the port Certbot listens on. " + "A conforming ACME server will still attempt to connect on port 80.") class IInstaller(IPlugin): From a8b2880963f0165585b0e30250709c450baac184 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2016 18:45:16 -0700 Subject: [PATCH 151/331] fix ip logging prompt for manual-test-mode --- certbot/plugins/manual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 9025dc9cb..2ef49d7f4 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -249,7 +249,7 @@ s.serve_forever()" """ def _get_ip_logging_permission(self): # pylint: disable=missing-docstring - if not self.conf("public-ip-logging-ok"): + if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")): if not zope.component.getUtility(interfaces.IDisplay).yesno( self.IP_DISCLAIMER, "Yes", "No", cli_flag="--manual-public-ip-logging-ok"): From cb982af63580b413aca8c7dca52a413423c8ceb9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 14 Sep 2016 10:27:39 -0700 Subject: [PATCH 152/331] put dnspython in alphabetical order --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 19297d38d..2033a9ee5 100644 --- a/tox.ini +++ b/tox.ini @@ -32,10 +32,10 @@ setenv = deps = py{26,27}-oldest: cryptography==0.8 py{26,27}-oldest: configargparse==0.10.0 + py{26,27}-oldest: dnspython>=1.12 py{26,27}-oldest: psutil==2.1.0 py{26,27}-oldest: PyOpenSSL==0.13 py{26,27}-oldest: python2-pythondialog==3.2.2rc1 - py{26,27}-oldest: dnspython>=1.12 [testenv:py33] commands = From cd74a07edfaf676b7eb30f179cb1d8e0da0ebd89 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 14 Sep 2016 10:31:31 -0700 Subject: [PATCH 153/331] Fix Travis tests due to cffi error --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 2033a9ee5..ac39e995e 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,10 @@ setenv = PYTHONHASHSEED = 0 # https://testrun.org/tox/latest/example/basic.html#special-handling-of-pythonhas +# cffi<=1.7 is required for oldest tests due to +# https://bitbucket.org/cffi/cffi/commits/18cdf37d6b2691301a15b0e54f49757ebd4ed0f2?at=default deps = + py{26,27}-oldest: cffi<=1.7 py{26,27}-oldest: cryptography==0.8 py{26,27}-oldest: configargparse==0.10.0 py{26,27}-oldest: dnspython>=1.12 From b7853f20316e38df6dfbff182f1f6f2d00fdf3cf Mon Sep 17 00:00:00 2001 From: Aidin Gharibnavaz Date: Thu, 15 Sep 2016 18:34:27 +0430 Subject: [PATCH 154/331] Issue #3239: Checking signal's default action before handling it. --- certbot/error_handler.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 4e6f6afac..3b5a38031 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -15,9 +15,14 @@ logger = logging.getLogger(__name__) # potentially occur from inside Python. Signals such as SIGILL were not # included as they could be a sign of something devious and we should terminate # immediately. -_SIGNALS = ([signal.SIGTERM] if os.name == "nt" else - [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, - signal.SIGXCPU, signal.SIGXFSZ]) +_SIGNALS = [signal.SIGTERM] +if os.name != "nt": + for signal_code in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, + signal.SIGXCPU, signal.SIGXFSZ]: + # Adding only those signals that their default action is not Ignore. + # This is platform-dependent, so we check it dynamically. + if signal.getsignal(signal_code) != signal.SIG_IGN: + _SIGNALS.append(signal_code) class ErrorHandler(object): From 6e0a68f844d933719472493a697215f1db9fb1f3 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 16 Sep 2016 14:21:13 -0700 Subject: [PATCH 155/331] Improve error message for "no installer plugin." --- certbot/plugins/selection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index a7388c4e1..dea9d970c 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -261,9 +261,10 @@ def diagnose_configurator_problem(cfg_type, requested, plugins): "your existing configuration.\nThe error was: {1!r}" .format(requested, plugins[requested].problem)) elif cfg_type == "installer": - msg = ('No installer plugins seem to be present and working on your system; ' - 'fix that or try running certbot with the "certonly" command to obtain' - ' a certificate you can install manually') + msg = ('Certbot doesn\'t know how to automatically configure the web ' + 'server on this system. However, it can still get a certificate for ' + 'you. Please run "certbot[-auto] certonly" to do so. You\'ll need to ' + 'manually configure your web server to use the resulting certificate.') else: msg = "{0} could not be determined or is not installed".format(cfg_type) raise errors.PluginSelectionError(msg) From 72ca219097b3ac13af154d8f9487dda4e22bd6a3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 16 Sep 2016 15:05:39 -0700 Subject: [PATCH 156/331] fix typo -- domains should be domain --- certbot/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/util.py b/certbot/util.py index e78ae664c..324c0b26a 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -395,7 +395,7 @@ def enforce_domain_sanity(domain): the requirements are not met. :param domain: Domain to check - :type domains: `str` or `unicode` + :type domain: `str` or `unicode` :raises ConfigurationError: for invalid domains and cases where Let's Encrypt currently will not issue certificates From f2e0afc96c1fc1e40f44f983ce8274cb8dd311c4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 16 Sep 2016 16:08:51 -0700 Subject: [PATCH 157/331] add enforce_le_validity function --- certbot/tests/util_test.py | 25 +++++++++++++++++++++++++ certbot/util.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 4aa6d3ff3..47d764ed5 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -330,6 +330,31 @@ class AddDeprecatedArgumentTest(unittest.TestCase): self.assertTrue("--old-option" not in stdout.getvalue()) +class EnforceLeValidity(unittest.TestCase): + """Test enforce_le_validity.""" + def _call(self, domain): + from certbot.util import enforce_le_validity + return enforce_le_validity(domain) + + def test_sanity(self): + self.assertRaises(errors.ConfigurationError, self._call, u"..") + + def test_invalid_chars(self): + self.assertRaises( + errors.ConfigurationError, self._call, u"hello_world.example.com") + + def test_leading_hyphen(self): + self.assertRaises( + errors.ConfigurationError, self._call, u"-a.example.com") + + def test_trailing_hyphen(self): + self.assertRaises( + errors.ConfigurationError, self._call, u"a-.example.com") + + def test_valid_domain(self): + self.assertEqual(self._call(u"example.com"), u"example.com") + + class EnforceDomainSanityTest(unittest.TestCase): """Test enforce_domain_sanity.""" diff --git a/certbot/util.py b/certbot/util.py index 324c0b26a..0b1431d98 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -390,6 +390,34 @@ def add_deprecated_argument(add_argument, argument_name, nargs): help=argparse.SUPPRESS, nargs=nargs) +def enforce_le_validity(domain): + """Checks that Let's Encrypt will consider domain to be valid. + + :param str domain: FQDN to check + :type domain: `str` or `unicode` + :returns: The domain cast to `str`, with ASCII-only contents + :rtype: str + :raises ConfigurationError: for invalid domains and cases where Let's + Encrypt currently will not issue certificates + + """ + domain = enforce_domain_sanity(domain) + if not re.match("^[A-Za-z0-9.-]*$", domain): + raise errors.ConfigurationError( + "{0} contains an invalid character. " + "Valid characters are A-Z, a-z, 0-9, ., and -.".format(domain)) + for label in domain.split("."): + if label.startswith("-"): + raise errors.ConfigurationError( + 'label "{0}" in domain "{1}" cannot start with "-"'.format( + label, domain)) + if label.endswith("-"): + raise errors.ConfigurationError( + 'label "{0}" in domain "{1}" cannot end with "-"'.format( + label, domain)) + return domain + + def enforce_domain_sanity(domain): """Method which validates domain value and errors out if the requirements are not met. From 275e3f748e610740e99c12b503110b6cbbd3f666 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 16 Sep 2016 16:47:02 -0700 Subject: [PATCH 158/331] filter names returned by get_all_names --- certbot-nginx/certbot_nginx/configurator.py | 20 ++++++++++++++++++- .../certbot_nginx/tests/configurator_test.py | 6 ++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 444e48903..a89e276c5 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -308,7 +308,25 @@ class NginxConfigurator(common.Plugin): except (socket.error, socket.herror, socket.timeout): continue - return all_names + return self._get_filtered_names(all_names) + + def _get_filtered_names(self, all_names): + """Removes names that aren't considered valid by Let's Encrypt. + + :param set all_names: all names found in the Nginx configuration + + :returns: all found names that are considered valid by LE + :rtype: set + + """ + filtered_names = set() + for name in all_names: + try: + filtered_names.add(util.enforce_le_validity(name)) + except errors.ConfigurationError as error: + logger.debug('Not suggesting name "%s"', name) + logger.debug(error) + return filtered_names def _get_snakeoil_paths(self): # TODO: generate only once diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 9e0c0dda5..84f5e2e3b 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -66,10 +66,8 @@ class NginxConfiguratorTest(util.NginxTest): mock_gethostbyaddr.return_value = ('155.225.50.69.nephoscale.net', [], []) names = self.config.get_all_names() self.assertEqual(names, set( - ["*.www.foo.com", "somename", "another.alias", - "alias", "localhost", ".example.com", r"~^(www\.)?(example|bar)\.", - "155.225.50.69.nephoscale.net", "*.www.example.com", - "example.*", "www.example.org", "myhost"])) + ["somename", "another.alias", "alias", "localhost", + "155.225.50.69.nephoscale.net", "www.example.org", "myhost"])) def test_supported_enhancements(self): self.assertEqual(['redirect'], self.config.supported_enhancements()) From 307b2e5307002070ec33e3efabbc26775b3a8973 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 16 Sep 2016 16:53:25 -0700 Subject: [PATCH 159/331] Reject domains with only one label --- certbot-nginx/certbot_nginx/tests/configurator_test.py | 4 ++-- certbot/tests/util_test.py | 3 +++ certbot/util.py | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 84f5e2e3b..d3bf52a2e 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -66,8 +66,8 @@ class NginxConfiguratorTest(util.NginxTest): mock_gethostbyaddr.return_value = ('155.225.50.69.nephoscale.net', [], []) names = self.config.get_all_names() self.assertEqual(names, set( - ["somename", "another.alias", "alias", "localhost", - "155.225.50.69.nephoscale.net", "www.example.org", "myhost"])) + ["155.225.50.69.nephoscale.net", + "www.example.org", "another.alias"])) def test_supported_enhancements(self): self.assertEqual(['redirect'], self.config.supported_enhancements()) diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 47d764ed5..6f06c8306 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -351,6 +351,9 @@ class EnforceLeValidity(unittest.TestCase): self.assertRaises( errors.ConfigurationError, self._call, u"a-.example.com") + def test_one_label(self): + self.assertRaises(errors.ConfigurationError, self._call, u"com") + def test_valid_domain(self): self.assertEqual(self._call(u"example.com"), u"example.com") diff --git a/certbot/util.py b/certbot/util.py index 0b1431d98..5cb4b4cad 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -406,7 +406,12 @@ def enforce_le_validity(domain): raise errors.ConfigurationError( "{0} contains an invalid character. " "Valid characters are A-Z, a-z, 0-9, ., and -.".format(domain)) - for label in domain.split("."): + + labels = domain.split(".") + if len(labels) < 2: + raise errors.ConfigurationError( + "{0} needs at least two labels".format(domain)) + for label in labels: if label.startswith("-"): raise errors.ConfigurationError( 'label "{0}" in domain "{1}" cannot start with "-"'.format( From 3eaafcecad22913169b67751728a19f3f05957b5 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 16 Sep 2016 18:45:21 -0700 Subject: [PATCH 160/331] Use cli_command to print. --- certbot/plugins/selection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index dea9d970c..3fbc510ba 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -261,10 +261,12 @@ def diagnose_configurator_problem(cfg_type, requested, plugins): "your existing configuration.\nThe error was: {1!r}" .format(requested, plugins[requested].problem)) elif cfg_type == "installer": + from certbot.cli import cli_command msg = ('Certbot doesn\'t know how to automatically configure the web ' 'server on this system. However, it can still get a certificate for ' - 'you. Please run "certbot[-auto] certonly" to do so. You\'ll need to ' - 'manually configure your web server to use the resulting certificate.') + 'you. Please run "{0} certonly" to do so. You\'ll need to ' + 'manually configure your web server to use the resulting ' + 'certificate.').format(cli_command) else: msg = "{0} could not be determined or is not installed".format(cfg_type) raise errors.PluginSelectionError(msg) From 7c79e962492524b9f2af8d036bdad9bab85f1c56 Mon Sep 17 00:00:00 2001 From: Aidin Gharibnavaz Date: Sat, 17 Sep 2016 21:55:36 +0430 Subject: [PATCH 161/331] Issue #3239: SIGTERM was added twice by mistake. --- certbot/error_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 3b5a38031..819d32405 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) # immediately. _SIGNALS = [signal.SIGTERM] if os.name != "nt": - for signal_code in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, + for signal_code in [signal.SIGHUP, signal.SIGQUIT, signal.SIGXCPU, signal.SIGXFSZ]: # Adding only those signals that their default action is not Ignore. # This is platform-dependent, so we check it dynamically. From 3a8a5598a3dca2f44f59443a437aa0d6897921d2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 19 Sep 2016 14:44:34 -0700 Subject: [PATCH 162/331] update constants.py --- certbot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/constants.py b/certbot/constants.py index 1ddb9fedf..ae998e15a 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -54,7 +54,7 @@ enhancements. List of expected options parameters: - redirect: None - http-header: TODO -- ocsp-stapling: TODO +- ocsp-stapling: certificate chain file path - spdy: TODO """ From fea079400fb643ca7b7bd94088bd5a1da925cc8b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 19 Sep 2016 14:55:26 -0700 Subject: [PATCH 163/331] Provide chain_path for enhance config --- certbot/client.py | 8 ++++++-- certbot/main.py | 4 ++-- certbot/tests/client_test.py | 38 ++++++++++++++++++------------------ 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index f92370d30..a4f2248a6 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -382,7 +382,8 @@ class Client(object): with error_handler.ErrorHandler(self._rollback_and_restart, msg): # sites may have been enabled / final cleanup self.installer.restart() - def enhance_config(self, domains, config): + + def enhance_config(self, domains, config, chain_path): """Enhance the configuration. :param list domains: list of domains to configure @@ -392,6 +393,9 @@ class Client(object): it must have the redirect, hsts and uir attributes. :type namespace: :class:`argparse.Namespace` + :param chain_path: chain file path + :type chain_path: `str` or `None` + :raises .errors.Error: if no installer is specified in the client. @@ -425,7 +429,7 @@ class Client(object): self.apply_enhancement(domains, "ensure-http-header", "Upgrade-Insecure-Requests") if staple: - self.apply_enhancement(domains, "staple-ocsp") + self.apply_enhancement(domains, "staple-ocsp", chain_path) msg = ("We were unable to restart web server") if redirect or hsts or uir or staple: diff --git a/certbot/main.py b/certbot/main.py index 511046df0..6c2455a18 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -436,7 +436,7 @@ def install(config, plugins): le_client.deploy_certificate( domains, config.key_path, config.cert_path, config.chain_path, config.fullchain_path) - le_client.enhance_config(domains, config) + le_client.enhance_config(domains, config, config.chain_path) def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print @@ -516,7 +516,7 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals domains, lineage.privkey, lineage.cert, lineage.chain, lineage.fullchain) - le_client.enhance_config(domains, config) + le_client.enhance_config(domains, config, lineage.chain) if len(lineage.available_versions("cert")) == 1: display_ops.success_installation(domains) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index e5c6b3af9..dd4211367 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -317,15 +317,15 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config(self, mock_enhancements): config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.Error, - self.client.enhance_config, ["foo.bar"], config) + self.assertRaises(errors.Error, self.client.enhance_config, + ["foo.bar"], config, None) mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer installer.supported_enhancements.return_value = ["redirect"] - self.client.enhance_config(["foo.bar"], config) + self.client.enhance_config(["foo.bar"], config, None) installer.enhance.assert_called_once_with("foo.bar", "redirect", None) self.assertEqual(installer.save.call_count, 1) installer.restart.assert_called_once_with() @@ -333,8 +333,8 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config_no_ask(self, mock_enhancements): config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.Error, - self.client.enhance_config, ["foo.bar"], config) + self.assertRaises(errors.Error, self.client.enhance_config, + ["foo.bar"], config, None) mock_enhancements.ask.return_value = True installer = mock.MagicMock() @@ -342,16 +342,16 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect", "ensure-http-header"] config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.client.enhance_config(["foo.bar"], config) + self.client.enhance_config(["foo.bar"], config, None) installer.enhance.assert_called_with("foo.bar", "redirect", None) config = ConfigHelper(redirect=False, hsts=True, uir=False) - self.client.enhance_config(["foo.bar"], config) + self.client.enhance_config(["foo.bar"], config, None) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Strict-Transport-Security") config = ConfigHelper(redirect=False, hsts=False, uir=True) - self.client.enhance_config(["foo.bar"], config) + self.client.enhance_config(["foo.bar"], config, None) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Upgrade-Insecure-Requests") @@ -365,14 +365,14 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = [] config = ConfigHelper(redirect=None, hsts=True, uir=True) - self.client.enhance_config(["foo.bar"], config) + self.client.enhance_config(["foo.bar"], config, None) installer.enhance.assert_not_called() mock_enhancements.ask.assert_not_called() def test_enhance_config_no_installer(self): config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.Error, - self.client.enhance_config, ["foo.bar"], config) + self.assertRaises(errors.Error, self.client.enhance_config, + ["foo.bar"], config, None) @mock.patch("certbot.client.zope.component.getUtility") @mock.patch("certbot.client.enhancements") @@ -386,8 +386,8 @@ class ClientTest(unittest.TestCase): config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.PluginError, - self.client.enhance_config, ["foo.bar"], config) + self.assertRaises(errors.PluginError, self.client.enhance_config, + ["foo.bar"], config, None) installer.recovery_routine.assert_called_once_with() self.assertEqual(mock_get_utility().add_message.call_count, 1) @@ -403,8 +403,8 @@ class ClientTest(unittest.TestCase): config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.PluginError, - self.client.enhance_config, ["foo.bar"], config) + self.assertRaises(errors.PluginError, self.client.enhance_config, + ["foo.bar"], config, None) installer.recovery_routine.assert_called_once_with() self.assertEqual(mock_get_utility().add_message.call_count, 1) @@ -420,8 +420,8 @@ class ClientTest(unittest.TestCase): config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.PluginError, - self.client.enhance_config, ["foo.bar"], config) + self.assertRaises(errors.PluginError, self.client.enhance_config, + ["foo.bar"], config, None) self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() @@ -440,8 +440,8 @@ class ClientTest(unittest.TestCase): config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.PluginError, - self.client.enhance_config, ["foo.bar"], config) + self.assertRaises(errors.PluginError, self.client.enhance_config, + ["foo.bar"], config, None) self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) From dff1368765ae0d4a64681c289e72e8fa162c5d95 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 19 Sep 2016 15:05:16 -0700 Subject: [PATCH 164/331] add staple-ocsp test --- certbot/tests/client_test.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index dd4211367..5e398c2cd 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -332,31 +332,41 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config_no_ask(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, + uir=False, staple=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config, None) mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer - installer.supported_enhancements.return_value = ["redirect", "ensure-http-header"] + installer.supported_enhancements.return_value = [ + "redirect", "ensure-http-header", "staple-ocsp"] - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, + uir=False, staple=False) self.client.enhance_config(["foo.bar"], config, None) installer.enhance.assert_called_with("foo.bar", "redirect", None) - config = ConfigHelper(redirect=False, hsts=True, uir=False) + config = ConfigHelper(redirect=False, hsts=True, + uir=False, staple=False) self.client.enhance_config(["foo.bar"], config, None) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Strict-Transport-Security") - config = ConfigHelper(redirect=False, hsts=False, uir=True) + config = ConfigHelper(redirect=False, hsts=False, + uir=True, staple=False) self.client.enhance_config(["foo.bar"], config, None) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Upgrade-Insecure-Requests") - self.assertEqual(installer.save.call_count, 3) - self.assertEqual(installer.restart.call_count, 3) + config = ConfigHelper(redirect=False, hsts=False, + uir=False, staple=True) + self.client.enhance_config(["foo.bar"], config, None) + installer.enhance.assert_called_with("foo.bar", "staple-ocsp", None) + + self.assertEqual(installer.save.call_count, 4) + self.assertEqual(installer.restart.call_count, 4) @mock.patch("certbot.client.enhancements") def test_enhance_config_unsupported(self, mock_enhancements): From 2bdf1258ad9db1a3a0681a3b81614c93a1d5b96d Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 19 Sep 2016 15:43:26 -0700 Subject: [PATCH 165/331] Include log retention count to 1000. This is a quick fix to https://github.com/certbot/certbot/issues/3382, so that Python log rotation is less likely to delete files before logrotate can get to them. Specifically, if logrotate's config says to preserve fewer than 1000 logs, it will do the right thing. --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 511046df0..a5448a005 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -601,7 +601,7 @@ def setup_log_file_handler(config, logfile, fmt): log_file_path = os.path.join(config.logs_dir, logfile) try: handler = logging.handlers.RotatingFileHandler( - log_file_path, maxBytes=2 ** 20, backupCount=10) + log_file_path, maxBytes=2 ** 20, backupCount=1000) except IOError as error: raise errors.Error(_PERM_ERR_FMT.format(error)) # rotate on each invocation, rollover only possible when maxBytes From a18a8f051d8d32bc92bc24fb6f41a30f3a89be3b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Sep 2016 15:01:01 -0700 Subject: [PATCH 166/331] Improve documentation for --preferred-challenges --- certbot/cli.py | 10 ++++----- docs/using.rst | 55 ++++++++++++++++++++++++++++---------------------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 942fc8ac2..0fbf6eefb 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -794,7 +794,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis default=flag_default("tls_sni_01_port"), help=config_help("tls_sni_01_port")) helpful.add( - ["certonly", "renew", "run"], "--http-01-port", type=int, + ["certonly", "renew", "run", "manual"], "--http-01-port", type=int, dest="http01_port", default=flag_default("http01_port"), help=config_help("http01_port")) helpful.add( @@ -848,13 +848,13 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") helpful.add( - ["certonly", "renew", "run"], "--preferred-challenges", + ["manual", "certonly", "renew", "run"], "--preferred-challenges", dest="pref_challs", action=_PrefChallAction, default=[], help="A sorted, comma delimited list of the preferred challenge to " "use during authorization with the most preferred challenge " - "listed first (e.g. tls-sni-01,http-01). If none of the " - "preferred challenges can be used by the selected plugin to " - "satisfy the CA, authorization is not attempted.") + 'listed first. Eg, "dns-01" or "tls-sni-01,http-01,dns-01").' + ' Not all plugins support all challenges. See ' + 'https://certbot.eff.org/docs/using.html#plugins for details.') helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." diff --git a/docs/using.rst b/docs/using.rst index 20b6cc5c7..972c7248f 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -60,7 +60,7 @@ an alternate method fo install ``certbot``. Certbot-Auto ^^^^^^^^^^^^ -The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies +The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies from your web server OS and putting others in a python virtual environment. You can download and run it as follows:: @@ -77,8 +77,8 @@ download and run it as follows:: The ``certbot-auto`` command updates to the latest client release automatically. Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly -the same command line flags and arguments. For more information, see -`Certbot command-line options `_. +the same command line flags and arguments. For more information, see +`Certbot command-line options `_. Running with Docker ^^^^^^^^^^^^^^^^^^^ @@ -88,8 +88,8 @@ certificate. However, this mode of operation is unable to install certificates or configure your webserver, because our installer plugins cannot reach your webserver from inside the Docker container. -Most users should use the operating system packages (see instructions at -certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only +Most users should use the operating system packages (see instructions at +certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only use Docker if you are sure you know what you are doing and have a good reason to do so. @@ -113,12 +113,12 @@ to, `install Docker`_, then issue the following command: quay.io/letsencrypt/letsencrypt:latest certonly Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory -``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from +``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from within Docker, you must install the certificate manually according to the procedure recommended by the provider of your webserver. -For more information about the layout -of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. +For more information about the layout +of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. .. _Docker: https://docker.com .. _`install Docker`: https://docs.docker.com/userguide/ @@ -242,8 +242,8 @@ whole process is described in the :doc:`contributing`. .. _plugins: -Getting certificates -==================== +Getting certificates (and chosing plugins) +========================================== The Certbot client supports a number of different "plugins" that can be used to obtain and/or install certificates. @@ -252,34 +252,41 @@ Plugins that can obtain a cert are called "authenticators" and can be used with the "certonly" command. This will carry out the steps needed to validate that you control the domain(s) you are requesting a cert for, obtain a cert for the specified domain(s), and place it in the ``/etc/letsencrypt`` directory on your -machine - without editing any of your server's configuration files to serve the +machine - without editing any of your server's configuration files to serve the obtained certificate. If you specify multiple domains to authenticate, they will all be listed in a single certificate. To obtain multiple seperate certificates you will need to run Certbot multiple times. -Plugins that can install a cert are called "installers" and can be used with the +Plugins that can install a cert are called "installers" and can be used with the "install" command. These plugins can modify your webserver's configuration to -serve your website over HTTPS using certificates obtained by certbot. +serve your website over HTTPS using certificates obtained by certbot. Plugins that do both can be used with the "certbot run" command, which is the default when no command is specified. The "run" subcommand can also be used to specify a combination of distinct authenticator and installer plugins. -=========== ==== ==== =============================================================== -Plugin Auth Inst Notes -=========== ==== ==== =============================================================== -apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on +=========== ==== ==== =============================================================== ============================= +Plugin Auth Inst Notes Challenge types (and port) +=========== ==== ==== =============================================================== ============================= +apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on ``tls-sni-01`` (443) Debian-based distributions with ``libaugeas0`` 1.0+. -webroot_ Y N Obtains a cert by writing to the webroot directory of an +webroot_ Y N Obtains a cert by writing to the webroot directory of an ``http-01`` (80) already running webserver. -standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires - port 80 or 443 to be available. This is useful on systems +standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires ``http-01`` (80) or + port 80 or 443 to be available. This is useful on systems ``tls-sni-01`` (443) with no webserver, or when direct integration with the local webserver is not supported or not desired. -manual_ Y N Helps you obtain a cert by giving you instructions to perform - domain validation yourself. -nginx_ Y Y Very experimental and not included in certbot-auto_. -=========== ==== ==== =============================================================== +manual_ Y N Helps you obtain a cert by giving you instructions to perform ``http-01`` (80) or + domain validation yourself. ``dns-01`` (53) +nginx_ Y Y Very experimental and not included in certbot-auto_. ``tls-sni-01`` (443) +=========== ==== ==== =============================================================== ============================= + +Under the hood, plugins use one of several "Challenge Types" to prove you control a domain. +The options are ``http-01`` (which uses port 80), ``tls-sni-01`` (port 443) and ``dns-01`` +(requring configuration of a DNS server on port 53, thought that's often not +the same machine as your webserver). A few plugins support more than one +challenge type, in which case you can choose it with +``--preferred-challenges``. There are also many third-party-plugins_ available. Below we describe in more detail the circumstances in which each plugin can be used, and how to use it. From 741a57c976f53f36e38a91c252186f5098e941df Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Sep 2016 15:02:03 -0700 Subject: [PATCH 167/331] Accept --preferred-challenges "dns-01, http-01" --- certbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index 0fbf6eefb..4d91a1904 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1048,7 +1048,7 @@ class _PrefChallAction(argparse.Action): """Action class for parsing preferred challenges.""" def __call__(self, parser, namespace, pref_challs, option_string=None): - challs = pref_challs.split(",") + challs = [c.strip() for c in pref_challs.split(",")] unrecognized = ", ".join(name for name in challs if name not in challenges.Challenge.TYPES) if unrecognized: From 9219617b0c5f7628f2cfff2561ac2ad32251b3e1 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Sep 2016 15:20:06 -0700 Subject: [PATCH 168/331] Nicer table formatting --- docs/using.rst | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 972c7248f..e5c6d43bb 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -268,29 +268,33 @@ a combination of distinct authenticator and installer plugins. =========== ==== ==== =============================================================== ============================= Plugin Auth Inst Notes Challenge types (and port) =========== ==== ==== =============================================================== ============================= -apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on ``tls-sni-01`` (443) - Debian-based distributions with ``libaugeas0`` 1.0+. -webroot_ Y N Obtains a cert by writing to the webroot directory of an ``http-01`` (80) - already running webserver. -standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires ``http-01`` (80) or - port 80 or 443 to be available. This is useful on systems ``tls-sni-01`` (443) - with no webserver, or when direct integration with the local - webserver is not supported or not desired. -manual_ Y N Helps you obtain a cert by giving you instructions to perform ``http-01`` (80) or - domain validation yourself. ``dns-01`` (53) -nginx_ Y Y Very experimental and not included in certbot-auto_. ``tls-sni-01`` (443) +apache_ Y Y | Automates obtaining and installing a cert with Apache 2.4 on tls-sni-01_ (443) + | Debian-based distributions with ``libaugeas0`` 1.0+. +webroot_ Y N | Obtains a cert by writing to the webroot directory of an http-01_ (80) + | already running webserver. +standalone_ Y N | Uses a "standalone" webserver to obtain a cert. Requires http-01_ (80) or + | port 80 or 443 to be available. This is useful on systems tls-sni-01_ (443) + | with no webserver, or when direct integration with the local + | webserver is not supported or not desired. +manual_ Y N | Helps you obtain a cert by giving you instructions to perform http-01_ (80) or + | domain validation yourself. dns-01_ (53) +nginx_ Y Y | Very experimental and not included in certbot-auto_. tls-sni-01_ (443) =========== ==== ==== =============================================================== ============================= Under the hood, plugins use one of several "Challenge Types" to prove you control a domain. -The options are ``http-01`` (which uses port 80), ``tls-sni-01`` (port 443) and ``dns-01`` +The options are http-01_ (which uses port 80), tls-sni-01_ (port 443) and dns-01_ (requring configuration of a DNS server on port 53, thought that's often not the same machine as your webserver). A few plugins support more than one -challenge type, in which case you can choose it with +challenge type, in which case you can choose one with ``--preferred-challenges``. There are also many third-party-plugins_ available. Below we describe in more detail the circumstances in which each plugin can be used, and how to use it. +.. _tls-sni-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3 +.. _http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#page-40 +.. _dns-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.4 + Apache ------ From 6c066ef10cffa58c75b60f0827f4c9231993b898 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Sep 2016 15:23:42 -0700 Subject: [PATCH 169/331] Better section link --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index e5c6d43bb..8e9524634 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -292,7 +292,7 @@ There are also many third-party-plugins_ available. Below we describe in more de the circumstances in which each plugin can be used, and how to use it. .. _tls-sni-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3 -.. _http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#page-40 +.. _http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.2 .. _dns-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.4 Apache From 8b553fa88f6cb03722695c4be296466ba8b59178 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 21 Sep 2016 15:38:37 -0700 Subject: [PATCH 170/331] tie oscp stapling to enhancements system --- certbot-nginx/certbot_nginx/configurator.py | 71 +++++++++++-------- .../certbot_nginx/tests/configurator_test.py | 64 ++++++++--------- 2 files changed, 73 insertions(+), 62 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 444e48903..cc37e5bfe 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -94,7 +94,8 @@ class NginxConfigurator(common.Plugin): # These will be set in the prepare function self.parser = None self.version = version - self._enhance_func = {"redirect": self._enable_redirect} + self._enhance_func = {"redirect": self._enable_redirect, + "staple-ocsp": self._enable_ocsp_stapling} # Set up reverter self.reverter = reverter.Reverter(self.config) @@ -137,11 +138,6 @@ class NginxConfigurator(common.Plugin): .. note:: Aborts if the vhost is missing ssl_certificate or ssl_certificate_key. - .. note:: Nginx doesn't have a cert chain directive. - It expects the cert file to have the concatenated chain. - However, we use the chain file as input to the - ssl_trusted_certificate directive, used for verify OCSP responses. - .. note:: This doesn't save the config files! :raises errors.PluginError: When unable to deploy certificate due to @@ -157,26 +153,9 @@ class NginxConfigurator(common.Plugin): 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 = [ - ['\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( - "--chain-path is required to enable " - "Online Certificate Status Protocol (OCSP) stapling " - "on nginx >= 1.3.7.") - try: self.parser.add_server_directives(vhost.filep, vhost.names, cert_directives, replace=True) - self.parser.add_server_directives(vhost.filep, vhost.names, - stapling_directives, replace=False) logger.info("Deployed Certificate to VirtualHost %s for %s", vhost.filep, vhost.names) except errors.MisconfigurationError as error: @@ -192,12 +171,6 @@ class NginxConfigurator(common.Plugin): ", ".join(str(addr) for addr in vhost.addrs))) self.save_notes += "\tssl_certificate %s\n" % fullchain_path self.save_notes += "\tssl_certificate_key %s\n" % key_path - if len(stapling_directives) > 0: - self.save_notes += "\tssl_trusted_certificate %s\n" % chain_path - self.save_notes += "\tssl_stapling on\n" - self.save_notes += "\tssl_stapling_verify on\n" - - ####################### # Vhost parsing methods @@ -394,6 +367,7 @@ class NginxConfigurator(common.Plugin): "Unsupported enhancement: {0}".format(enhancement)) except errors.PluginError: logger.warning("Failed %s for %s", enhancement, domain) + raise def _enable_redirect(self, vhost, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. @@ -417,6 +391,45 @@ class NginxConfigurator(common.Plugin): vhost.filep, vhost.names, redirect_block, replace=False) logger.info("Redirecting all traffic to ssl in %s", vhost.filep) + def _enable_ocsp_stapling(self, vhost, chain_path): + """Include OCSP response in TLS handshake + + :param vhost: Destination of traffic, an ssl enabled vhost + :type vhost: :class:`~certbot_nginx.obj.VirtualHost` + + :param chain_path: chain file path + :type chain_path: `str` or `None` + + """ + if self.version < (1, 3, 7): + raise errors.PluginError("Version 1.3.7 or greater of nginx " + "is needed to enable OCSP stapling") + + if chain_path is None: + raise errors.PluginError( + "--chain-path is required to enable " + "Online Certificate Status Protocol (OCSP) stapling " + "on nginx >= 1.3.7.") + + stapling_directives = [ + ['\n ', 'ssl_trusted_certificate', ' ', chain_path], + ['\n ', 'ssl_stapling', ' ', 'on'], + ['\n ', 'ssl_stapling_verify', ' ', 'on'], ['\n']] + + try: + self.parser.add_server_directives(vhost.filep, vhost.names, + stapling_directives, replace=False) + except errors.MisconfigurationError as error: + logger.debug(error) + raise errors.PluginError("An error occurred while enabling OCSP " + "stapling for {0}.".format(vhost.names)) + + self.save_notes += ("OCSP Stapling was enabled " + "on SSL Vhost: {0}.\n".format(vhost.filep)) + self.save_notes += "\tssl_trusted_certificate {0}\n".format(chain_path) + self.save_notes += "\tssl_stapling on\n" + self.save_notes += "\tssl_stapling_verify on\n" + ###################################### # Nginx server management (IInstaller) ###################################### diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 9e0c0dda5..43226213b 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -141,37 +141,6 @@ class NginxConfiguratorTest(util.NginxTest): def test_more_info(self): self.assertTrue('nginx.conf' in self.config.more_info()) - def test_deploy_cert_stapling(self): - # Choose a version of Nginx greater than 1.3.7 so stapling code gets - # invoked. - self.config.version = (1, 9, 6) - example_conf = self.config.parser.abs_path('sites-enabled/example.com') - self.config.deploy_cert( - "www.example.com", - "example/cert.pem", - "example/key.pem", - "example/chain.pem", - "example/fullchain.pem") - self.config.save() - self.config.parser.load() - generated_conf = self.config.parser.parsed[example_conf] - - self.assertTrue(util.contains_at_depth(generated_conf, - ['ssl_stapling', 'on'], 2)) - self.assertTrue(util.contains_at_depth(generated_conf, - ['ssl_stapling_verify', 'on'], 2)) - self.assertTrue(util.contains_at_depth(generated_conf, - ['ssl_trusted_certificate', 'example/chain.pem'], 2)) - - def test_deploy_cert_stapling_requires_chain_path(self): - self.config.version = (1, 3, 7) - self.assertRaises(errors.PluginError, self.config.deploy_cert, - "www.example.com", - "example/cert.pem", - "example/key.pem", - None, - "example/fullchain.pem") - def test_deploy_cert_requires_fullchain_path(self): self.config.version = (1, 3, 1) self.assertRaises(errors.PluginError, self.config.deploy_cert, @@ -185,8 +154,6 @@ class NginxConfiguratorTest(util.NginxTest): server_conf = self.config.parser.abs_path('server.conf') nginx_conf = self.config.parser.abs_path('nginx.conf') example_conf = self.config.parser.abs_path('sites-enabled/example.com') - # Choose a version of Nginx less than 1.3.7 so stapling code doesn't get - # invoked. self.config.version = (1, 3, 1) # Get the default SSL vhost @@ -429,5 +396,36 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + def test_staple_ocsp_bad_version(self): + self.config.version = (1, 3, 1) + self.assertRaises(errors.PluginError, self.config.enhance, + "www.example.com", "staple-ocsp", "chain_path") + + def test_staple_ocsp_no_chain_path(self): + self.assertRaises(errors.PluginError, self.config.enhance, + "www.example.com", "staple-ocsp", None) + + def test_staple_ocsp_internal_error(self): + self.config.enhance("www.example.com", "staple-ocsp", "chain_path") + # error is raised because the server block has conflicting directives + self.assertRaises(errors.PluginError, self.config.enhance, + "www.example.com", "staple-ocsp", "different_path") + + def test_staple_ocsp(self): + chain_path = "example/chain.pem" + self.config.enhance("www.example.com", "staple-ocsp", chain_path) + + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + generated_conf = self.config.parser.parsed[example_conf] + + self.assertTrue(util.contains_at_depth( + generated_conf, + ['ssl_trusted_certificate', 'example/chain.pem'], 2)) + self.assertTrue(util.contains_at_depth( + generated_conf, ['ssl_stapling', 'on'], 2)) + self.assertTrue(util.contains_at_depth( + generated_conf, ['ssl_stapling_verify', 'on'], 2)) + + if __name__ == "__main__": unittest.main() # pragma: no cover From 93a9e8c836c4c731856e30475358ec7a6b6a56e6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 21 Sep 2016 15:48:24 -0700 Subject: [PATCH 171/331] list 'staple-ocsp' as supported in nginx --- certbot-nginx/certbot_nginx/configurator.py | 2 +- certbot-nginx/certbot_nginx/tests/configurator_test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index cc37e5bfe..2b5a10047 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -346,7 +346,7 @@ class NginxConfigurator(common.Plugin): ################################## def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" - return ['redirect'] + return ['redirect', 'staple-ocsp'] def enhance(self, domain, enhancement, options=None): """Enhance configuration. diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 43226213b..37e120612 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -72,7 +72,8 @@ class NginxConfiguratorTest(util.NginxTest): "example.*", "www.example.org", "myhost"])) def test_supported_enhancements(self): - self.assertEqual(['redirect'], self.config.supported_enhancements()) + self.assertEqual(['redirect', 'staple-ocsp'], + self.config.supported_enhancements()) def test_enhance(self): self.assertRaises( From 107a3e6aa93f19de3e3b117f6fd11a8ab049caab Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Sep 2016 16:17:31 -0700 Subject: [PATCH 172/331] Allow & document --preferred-challenges dns,http --- certbot/cli.py | 8 ++++++-- docs/using.rst | 13 +++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 4d91a1904..5fbafa51e 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -852,9 +852,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis dest="pref_challs", action=_PrefChallAction, default=[], help="A sorted, comma delimited list of the preferred challenge to " "use during authorization with the most preferred challenge " - 'listed first. Eg, "dns-01" or "tls-sni-01,http-01,dns-01").' + 'listed first. Eg, "dns" or "tls-sni-01,http,dns").' ' Not all plugins support all challenges. See ' - 'https://certbot.eff.org/docs/using.html#plugins for details.') + 'https://certbot.eff.org/docs/using.html#plugins for details.' + ' Challenges are versioned, but if you pick "http" rather than' + ' "http-01", Certbot will select the latest version automatically.' ) helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." @@ -1048,7 +1050,9 @@ class _PrefChallAction(argparse.Action): """Action class for parsing preferred challenges.""" def __call__(self, parser, namespace, pref_challs, option_string=None): + aliases = {"dns": "dns-01", "http": "http-01", "tls-sni": "tls-sni-01"} challs = [c.strip() for c in pref_challs.split(",")] + challs = [aliases[c] if c in aliases else c for c in challs] unrecognized = ", ".join(name for name in challs if name not in challenges.Challenge.TYPES) if unrecognized: diff --git a/docs/using.rst b/docs/using.rst index 8e9524634..18dca071a 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -281,16 +281,17 @@ manual_ Y N | Helps you obtain a cert by giving you instructions to pe nginx_ Y Y | Very experimental and not included in certbot-auto_. tls-sni-01_ (443) =========== ==== ==== =============================================================== ============================= -Under the hood, plugins use one of several "Challenge Types" to prove you control a domain. -The options are http-01_ (which uses port 80), tls-sni-01_ (port 443) and dns-01_ -(requring configuration of a DNS server on port 53, thought that's often not -the same machine as your webserver). A few plugins support more than one -challenge type, in which case you can choose one with -``--preferred-challenges``. +Under the hood, plugins use one of several ACME protocol "Challenges_" to +prove you control a domain. The options are http-01_ (which uses port 80), +tls-sni-01_ (port 443) and dns-01_ (requring configuration of a DNS server on +port 53, thought that's often not the same machine as your webserver). A few +plugins support more than one challenge type, in which case you can choose one +with ``--preferred-challenges``. There are also many third-party-plugins_ available. Below we describe in more detail the circumstances in which each plugin can be used, and how to use it. +.. _Challenges: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7 .. _tls-sni-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3 .. _http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.2 .. _dns-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.4 From f6c605cd15344e579a18e2aebd8f192ea4c5b43b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Sep 2016 16:43:35 -0700 Subject: [PATCH 173/331] Add tests for the --preferred-challenges cli parser --- certbot/tests/cli_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 2c6e32705..fdfb9dcc8 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -423,6 +423,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods namespace = parse(long_args) self.assertEqual(namespace.domains, ['example.com', 'another.net']) + def test_preferred_challenges(self): + from acme.challenges import HTTP01, TLSSNI01, DNS01 + parse = self._get_argument_parser() + + short_args = ['--preferred-challenges', 'http, tls-sni-01, dns'] + namespace = parse(short_args) + + self.assertEqual(namespace.pref_challs, [HTTP01, TLSSNI01, DNS01]) + + short_args = ['--preferred-challenges', 'jumping-over-the-moon'] + self.assertRaises(argparse.ArgumentTypeError, parse, short_args) + def test_server_flag(self): parse = self._get_argument_parser() namespace = parse('--server example.com'.split()) From e28017a1108d674ce834d254bf20765c5096ca86 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Sep 2016 16:45:42 -0700 Subject: [PATCH 174/331] Lint --- certbot/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 5fbafa51e..81f7819e2 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -855,8 +855,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis 'listed first. Eg, "dns" or "tls-sni-01,http,dns").' ' Not all plugins support all challenges. See ' 'https://certbot.eff.org/docs/using.html#plugins for details.' - ' Challenges are versioned, but if you pick "http" rather than' - ' "http-01", Certbot will select the latest version automatically.' ) + ' ACME Challenges are versioned, but if you pick "http" rather than' + ' "http-01", Certbot will select the latest version automatically.') helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." From 0165269c052965091b12edddc240b66fae87aedf Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 21 Sep 2016 17:37:47 -0700 Subject: [PATCH 175/331] denit cli.py --- certbot/cli.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 81f7819e2..78ac04b06 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -848,15 +848,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") helpful.add( - ["manual", "certonly", "renew", "run"], "--preferred-challenges", - dest="pref_challs", action=_PrefChallAction, default=[], - help="A sorted, comma delimited list of the preferred challenge to " - "use during authorization with the most preferred challenge " - 'listed first. Eg, "dns" or "tls-sni-01,http,dns").' - ' Not all plugins support all challenges. See ' + ["manual", "standalone", "certonly", "renew", "run"], + "--preferred-challenges", dest="pref_challs", + action=_PrefChallAction, default=[], + help='A sorted, comma delimited list of the preferred challenge to ' + 'use during authorization with the most preferred challenge ' + 'listed first (Eg, "dns" or "tls-sni-01,http,dns").' + 'Not all plugins support all challenges. See ' 'https://certbot.eff.org/docs/using.html#plugins for details.' - ' ACME Challenges are versioned, but if you pick "http" rather than' - ' "http-01", Certbot will select the latest version automatically.') + 'ACME Challenges are versioned, but if you pick "http" rather than' + '"http-01", Certbot will select the latest version automatically.') helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." From 74ac006f14959aebdf77a0b81182ebdc85042531 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 21 Sep 2016 17:48:17 -0700 Subject: [PATCH 176/331] fix spacing --- certbot/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 78ac04b06..83697d8da 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -853,11 +853,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis action=_PrefChallAction, default=[], help='A sorted, comma delimited list of the preferred challenge to ' 'use during authorization with the most preferred challenge ' - 'listed first (Eg, "dns" or "tls-sni-01,http,dns").' + 'listed first (Eg, "dns" or "tls-sni-01,http,dns"). ' 'Not all plugins support all challenges. See ' - 'https://certbot.eff.org/docs/using.html#plugins for details.' - 'ACME Challenges are versioned, but if you pick "http" rather than' - '"http-01", Certbot will select the latest version automatically.') + 'https://certbot.eff.org/docs/using.html#plugins for details. ' + 'ACME Challenges are versioned, but if you pick "http" rather ' + 'than "http-01", Certbot will select the latest version ' + 'automatically.') helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." From 3b9a87cd2fd2d565c002dde37419a6f8624b7a20 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 23 Sep 2016 17:48:50 -0700 Subject: [PATCH 177/331] If lineages are in an inconsistent (non-deployed) state, deploy them (#3533) * If lineages are in an inconsistent (non-deployed) state, deploy them * Test new _handle_identical_cert case * Lint * Fix find & replace SNAFU --- certbot/main.py | 23 ++++++++++++++--------- certbot/tests/cli_test.py | 1 + certbot/tests/main_test.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 675d10640..3557f65b3 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -155,27 +155,32 @@ def _handle_subset_cert_request(config, domains, cert): "reinvoke the client.") -def _handle_identical_cert_request(config, cert): - """Figure out what to do if a cert has the same names as a previously obtained one +def _handle_identical_cert_request(config, lineage): + """Figure out what to do if a lineage has the same names as a previously obtained one - :param storage.RenewableCert cert: + :param storage.RenewableCert lineage: :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal action can be: "newcert" | "renew" | "reinstall" :rtype: tuple """ - if renewal.should_renew(config, cert): - return "renew", cert + if lineage.has_pending_deployment(): + logger.warn("Found a new cert /archive/ that was not linked to in /live/; " + "fixing and reinstalling..") + lineage.update_all_links_to(lineage.latest_common_version()) + return "reinstall", lineage + if renewal.should_renew(config, lineage): + return "renew", lineage if config.reinstall: # Set with --reinstall, force an identical certificate to be # reinstalled without further prompting. - return "reinstall", cert + return "reinstall", lineage question = ( "You have an existing certificate that contains exactly the same " "domains you requested and isn't close to expiry." "{br}(ref: {0}){br}{br}What would you like to do?" - ).format(cert.configfile.filename, br=os.linesep) + ).format(lineage.configfile.filename, br=os.linesep) if config.verb == "run": keep_opt = "Attempt to reinstall this existing certificate" @@ -193,9 +198,9 @@ def _handle_identical_cert_request(config, cert): "User chose to cancel the operation and may " "reinvoke the client.") elif response[1] == 0: - return "reinstall", cert + return "reinstall", lineage elif response[1] == 1: - return "renew", cert + return "renew", lineage else: assert False, "This is impossible" diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index fdfb9dcc8..6cbbea631 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -579,6 +579,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path) mock_lineage.should_autorenew.return_value = due_for_renewal + mock_lineage.has_pending_deployment.return_value = False mock_certr = mock.MagicMock() mock_key = mock.MagicMock(pem='pem_key') mock_client = mock.MagicMock() diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 32df525f0..69291871a 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -11,6 +11,23 @@ from certbot import configuration from certbot import errors from certbot.plugins import disco as plugins_disco +class MainTest(unittest.TestCase): + def setUp(self): + pass + def tearDown(self): + pass + + @mock.patch("certbot.main.logger") + def test_handle_identical_cert_request_pending(self, _mock_logger): + # For now, just test has_pending_deployment_branch; other + # coverage is in cli_test.py... + from certbot import main + mock_lineage = mock.Mock() + mock_lineage.has_pending_deployment.return_value = True + # pylint: disable=protected-access + ret = main._handle_identical_cert_request(mock.Mock(), mock_lineage) + self.assertEqual(ret, ("reinstall", mock_lineage)) + self.assertEqual(mock_lineage.update_all_links_to.call_count, 1) class ObtainCertTest(unittest.TestCase): """Tests for certbot.main.obtain_cert.""" From 9f2dfc15feea55e0844aa046167979e163444c68 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Mon, 26 Sep 2016 13:13:29 -0700 Subject: [PATCH 178/331] Restructure how Nginx parser re-finds vhosts, and disable creating new server blocks. (#3528) * Restructure add_server_directives to take a vhost as argument. This is the first step towards fixing vhost selection in nginx. * Save path to vhost in file while parsing in get_vhosts(). * Disable creating a new server block when no names match. * Make parser select vhost based on information in the vhost it found previously, rather than searching again for a match. * Make add_server_directives update the passed vhost * Update boulder config to pass test * Add testing code for the _do_for_subarray function * documentation and formatting updates --- certbot-nginx/certbot_nginx/configurator.py | 22 ++-- certbot-nginx/certbot_nginx/obj.py | 9 +- certbot-nginx/certbot_nginx/parser.py | 97 ++++++--------- .../certbot_nginx/tests/configurator_test.py | 10 +- certbot-nginx/certbot_nginx/tests/obj_test.py | 4 +- .../certbot_nginx/tests/parser_test.py | 112 ++++++++++++------ .../certbot_nginx/tests/tls_sni_01_test.py | 6 +- .../tests/boulder-integration.conf.sh | 1 + 8 files changed, 134 insertions(+), 127 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 40e53a0e6..0a23f1f07 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -24,7 +24,6 @@ from certbot.plugins import common from certbot_nginx import constants from certbot_nginx import tls_sni_01 -from certbot_nginx import obj from certbot_nginx import parser @@ -154,7 +153,7 @@ class NginxConfigurator(common.Plugin): ['\n', 'ssl_certificate_key', ' ', key_path]] try: - self.parser.add_server_directives(vhost.filep, vhost.names, + self.parser.add_server_directives(vhost, cert_directives, replace=True) logger.info("Deployed Certificate to VirtualHost %s for %s", vhost.filep, vhost.names) @@ -198,12 +197,9 @@ class NginxConfigurator(common.Plugin): matches = self._get_ranked_matches(target_name) if not matches: - # No matches. Create a new vhost with this name in nginx.conf. - filep = self.parser.loc["root"] - 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])) + # No matches. Raise a misconfiguration error. + raise errors.MisconfigurationError( + "Cannot find a VirtualHost matching domain %s." % (target_name)) elif matches[0]['rank'] in xrange(2, 6): # Wildcard match - need to find the longest one rank = matches[0]['rank'] @@ -341,11 +337,7 @@ class NginxConfigurator(common.Plugin): self.parser.loc["ssl_options"]) self.parser.add_server_directives( - vhost.filep, vhost.names, ssl_block, replace=False) - vhost.ssl = True - vhost.raw.extend(ssl_block) - vhost.addrs.add(obj.Addr( - '', str(self.config.tls_sni_01_port), True, False)) + vhost, ssl_block, replace=False) def get_all_certs_keys(self): """Find all existing keys, certs from configuration. @@ -406,7 +398,7 @@ class NginxConfigurator(common.Plugin): '\n '] ], ['\n']] self.parser.add_server_directives( - vhost.filep, vhost.names, redirect_block, replace=False) + vhost, redirect_block, replace=False) logger.info("Redirecting all traffic to ssl in %s", vhost.filep) def _enable_ocsp_stapling(self, vhost, chain_path): @@ -435,7 +427,7 @@ class NginxConfigurator(common.Plugin): ['\n ', 'ssl_stapling_verify', ' ', 'on'], ['\n']] try: - self.parser.add_server_directives(vhost.filep, vhost.names, + self.parser.add_server_directives(vhost, stapling_directives, replace=False) except errors.MisconfigurationError as error: logger.debug(error) diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index f5ac88f6c..8c93d0a8b 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -107,10 +107,12 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods :ivar bool ssl: SSLEngine on in vhost :ivar bool enabled: Virtual host is enabled + :ivar list path: The indices into the parsed file used to access + the server block defining the vhost """ - def __init__(self, filep, addrs, ssl, enabled, names, raw): + def __init__(self, filep, addrs, ssl, enabled, names, raw, path): # pylint: disable=too-many-arguments """Initialize a VH.""" self.filep = filep @@ -119,6 +121,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.ssl = ssl self.enabled = enabled self.raw = raw + self.path = path def __str__(self): addr_str = ", ".join(str(addr) for addr in self.addrs) @@ -137,6 +140,8 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods return (self.filep == other.filep and list(self.addrs) == list(other.addrs) and self.names == other.names and - self.ssl == other.ssl and self.enabled == other.enabled) + self.ssl == other.ssl and + self.enabled == other.enabled and + self.path == other.path) return False diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 3919858d9..13bb38359 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -104,15 +104,15 @@ class NginxParser(object): # Find all the server blocks _do_for_subarray(tree, lambda x: x[0] == ['server'], - lambda x: srv.append(x[1])) + lambda x, y: srv.append((x[1], y))) # Find 'include' statements in server blocks and append their trees - for i, server in enumerate(servers[filename]): + for i, (server, path) in enumerate(servers[filename]): new_server = self._get_included_directives(server) - servers[filename][i] = new_server + servers[filename][i] = (new_server, path) for filename in servers: - for server in servers[filename]: + for server, path in servers[filename]: # Parse the server block into a VirtualHost object parsed_server = parse_server(server) @@ -121,7 +121,8 @@ class NginxParser(object): parsed_server['ssl'], enabled, parsed_server['names'], - server) + server, + path) vhosts.append(vhost) return vhosts @@ -240,42 +241,10 @@ class NginxParser(object): except IOError: logger.error("Could not open file for writing: %s", filename) - def _has_server_names(self, entry, names): - """Checks if a server block has the given set of server_names. This - is the primary way of identifying server blocks in the configurator. - Returns false if 'entry' doesn't look like a server block at all. + def add_server_directives(self, vhost, directives, replace): + """Add or replace directives in the server block identified by vhost. - ..todo :: Doesn't match server blocks whose server_name directives are - split across multiple conf files. - - :param list entry: The block to search - :param set names: The names to match - :rtype: bool - - """ - if len(names) == 0: - # Nothing to identify blocks with - return False - - if not isinstance(entry, list): - # Can't be a server block - return False - - new_entry = self._get_included_directives(entry) - server_names = set() - for item in new_entry: - if not isinstance(item, list): - # Can't be a server block - return False - - if len(item) > 0 and item[0] == 'server_name': - server_names.update(_get_servernames(item[1])) - - return server_names == names - - def add_server_directives(self, filename, names, directives, - replace): - """Add or replace directives in the first server block with names. + This method modifies vhost to be fully consistent with the new directives. ..note :: If replace is True, this raises a misconfiguration error if the directive does not already exist. @@ -285,34 +254,32 @@ class NginxParser(object): ..todo :: Doesn't match server blocks whose server_name directives are split across multiple conf files. - :param str filename: The absolute filename of the config file - :param set names: The server_name to match + :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 """ + filename = vhost.filep try: - _do_for_subarray(self.parsed[filename], - lambda x: self._has_server_names(x, names), - lambda x: _add_directives(x, directives, replace)) + result = self.parsed[filename] + for index in vhost.path: + result = result[index] + if not isinstance(result, list) or len(result) != 2: + raise errors.MisconfigurationError("Not a server block.") + result = result[1] + _add_directives(result, directives, replace) + + # update vhost based on new directives + new_server = self._get_included_directives(result) + parsed_server = parse_server(new_server) + vhost.addrs = parsed_server['addrs'] + vhost.ssl = parsed_server['ssl'] + vhost.names = parsed_server['names'] + vhost.raw = new_server except errors.MisconfigurationError as err: raise errors.MisconfigurationError("Problem in %s: %s" % (filename, err.message)) - def add_http_directives(self, filename, directives): - """Adds directives to the first encountered HTTP block in filename. - - We insert new directives at the top of the block to work around - https://trac.nginx.org/nginx/ticket/810: If the first server block - doesn't enable OCSP stapling, stapling is broken for all blocks. - - :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: x[1].insert(0, directives)) - def get_all_certs_keys(self): """Gets all certs and keys in the nginx config. @@ -341,7 +308,7 @@ class NginxParser(object): return c_k -def _do_for_subarray(entry, condition, func): +def _do_for_subarray(entry, condition, func, path=None): """Executes a function for a subarray of a nested array if it matches the given condition. @@ -350,12 +317,14 @@ def _do_for_subarray(entry, condition, func): :param function func: The function to call for each matching item """ + if path is None: + path = [] if isinstance(entry, list): if condition(entry): - func(entry) + func(entry, path) else: - for item in entry: - _do_for_subarray(item, condition, func) + for index, item in enumerate(entry): + _do_for_subarray(item, condition, func, path + [index]) def get_best_match(target_name, names): diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 4b0117806..9bb8a46d8 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -13,6 +13,7 @@ from acme import messages from certbot import achallenges from certbot import errors +from certbot_nginx import obj from certbot_nginx import parser from certbot_nginx.tests import util @@ -83,8 +84,12 @@ class NginxConfiguratorTest(util.NginxTest): def test_save(self): filep = self.config.parser.abs_path('sites-enabled/example.com') + mock_vhost = obj.VirtualHost(filep, + None, None, None, + set(['.example.com', 'example.*']), + None, [0]) self.config.parser.add_server_directives( - filep, set(['.example.com', 'example.*']), + mock_vhost, [['listen', ' ', '5001 ssl']], replace=False) self.config.save() @@ -135,7 +140,8 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(conf_path[name], path) for name in bad_results: - self.assertEqual(set([name]), self.config.choose_vhost(name).names) + self.assertRaises(errors.MisconfigurationError, + self.config.choose_vhost, name) def test_more_info(self): self.assertTrue('nginx.conf' in self.config.more_info()) diff --git a/certbot-nginx/certbot_nginx/tests/obj_test.py b/certbot-nginx/certbot_nginx/tests/obj_test.py index e7a993d1b..200f2acb9 100644 --- a/certbot-nginx/certbot_nginx/tests/obj_test.py +++ b/certbot-nginx/certbot_nginx/tests/obj_test.py @@ -80,7 +80,7 @@ class VirtualHostTest(unittest.TestCase): self.vhost1 = VirtualHost( "filep", set([Addr.fromstring("localhost")]), False, False, - set(['localhost']), []) + set(['localhost']), [], []) def test_eq(self): from certbot_nginx.obj import Addr @@ -88,7 +88,7 @@ class VirtualHostTest(unittest.TestCase): vhost1b = VirtualHost( "filep", set([Addr.fromstring("localhost blah")]), False, False, - set(['localhost']), []) + set(['localhost']), [], []) self.assertEqual(vhost1b, self.vhost1) self.assertEqual(str(vhost1b), str(self.vhost1)) diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 71807d4f4..18de59daf 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -79,6 +79,30 @@ class NginxParserTest(util.NginxTest): ['server_name', 'example.*']]]], parsed[0]) + def test__do_for_subarray(self): + # pylint: disable=protected-access + mylists = [([[2], [3], [2]], [[0], [2]]), + ([[2], [3], [4]], [[0]]), + ([[4], [3], [2]], [[2]]), + ([], []), + (2, []), + ([[[2], [3], [2]], [[2], [3], [2]]], + [[0, 0], [0, 2], [1, 0], [1, 2]]), + ([[[0], [3], [2]], [[2], [3], [2]]], [[0, 2], [1, 0], [1, 2]]), + ([[[0], [3], [4]], [[2], [3], [2]]], [[1, 0], [1, 2]]), + ([[[0], [3], [4]], [[5], [3], [2]]], [[1, 2]]), + ([[[0], [3], [4]], [[5], [3], [0]]], [])] + + for mylist, result in mylists: + paths = [] + parser._do_for_subarray(mylist, + lambda x: isinstance(x, list) and + len(x) >= 1 and + x[0] == 2, + lambda x, y, pts=paths: pts.append(y)) + self.assertEqual(paths, result) + + def test_get_vhosts(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) vhosts = nparser.get_vhosts() @@ -88,26 +112,28 @@ class NginxParserTest(util.NginxTest): False, True, set(['localhost', r'~^(www\.)?(example|bar)\.']), - []) + [], [9, 1, 9]) vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'), [obj.Addr('somename', '8080', False, False), obj.Addr('', '8000', False, False)], False, True, set(['somename', 'another.alias', 'alias']), - []) + [], [9, 1, 12]) vhost3 = obj.VirtualHost(nparser.abs_path('sites-enabled/example.com'), [obj.Addr('69.50.225.155', '9000', False, False), obj.Addr('127.0.0.1', '', False, False)], False, True, - set(['.example.com', 'example.*']), []) + set(['.example.com', 'example.*']), [], [0]) vhost4 = obj.VirtualHost(nparser.abs_path('sites-enabled/default'), [obj.Addr('myhost', '', False, True)], - False, True, set(['www.example.org']), []) + False, True, set(['www.example.org']), + [], [0]) vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'), [obj.Addr('*', '80', True, True)], True, True, set(['*.www.foo.com', - '*.www.example.com']), []) + '*.www.example.com']), + [], [2, 1, 0]) self.assertEqual(5, len(vhosts)) example_com = [x for x in vhosts if 'example.com' in x.filep][0] @@ -123,9 +149,12 @@ class NginxParserTest(util.NginxTest): def test_add_server_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) - nparser.add_server_directives(nparser.abs_path('nginx.conf'), - set(['localhost', + mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'), + None, None, None, + set(['localhost', r'~^(www\.)?(example|bar)\.']), + None, [9, 1, 9]) + nparser.add_server_directives(mock_vhost, [['foo', 'bar'], ['\n ', 'ssl_certificate', ' ', '/etc/ssl/cert.pem']], replace=False) @@ -133,47 +162,48 @@ class NginxParserTest(util.NginxTest): dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')]) self.assertEqual(1, len(re.findall(ssl_re, dump))) - server_conf = nparser.abs_path('server.conf') - names = set(['alias', 'another.alias', 'somename']) - nparser.add_server_directives(server_conf, names, + example_com = nparser.abs_path('sites-enabled/example.com') + names = set(['.example.com', 'example.*']) + mock_vhost.filep = example_com + mock_vhost.names = names + mock_vhost.path = [0] + nparser.add_server_directives(mock_vhost, [['foo', 'bar'], ['ssl_certificate', '/etc/ssl/cert2.pem']], replace=False) - nparser.add_server_directives(server_conf, names, [['foo', 'bar']], + nparser.add_server_directives(mock_vhost, [['foo', 'bar']], replace=False) from certbot_nginx.parser import COMMENT - self.assertEqual(nparser.parsed[server_conf], - [['server_name', 'somename alias another.alias'], - ['foo', 'bar'], - ['#', COMMENT], - ['ssl_certificate', '/etc/ssl/cert2.pem'], - ['#', COMMENT], - [], [] - ]) + self.assertEqual(nparser.parsed[example_com], + [[['server'], [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + ['foo', 'bar'], + ['#', COMMENT], + ['ssl_certificate', '/etc/ssl/cert2.pem'], + ['#', COMMENT], [], [] + ]]]) - 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) - root = nparser.parsed[filep] - self.assertTrue(util.contains_at_depth(root, ['http'], 1)) - self.assertTrue(util.contains_at_depth(root, block, 2)) - - # Check that our server block got inserted first among all server - # blocks. - http_block = [x for x in root if x[0] == ['http']][0][1] - server_blocks = [x for x in http_block if x[0] == ['server']] - self.assertEqual(server_blocks[0], block) + server_conf = nparser.abs_path('server.conf') + names = set(['alias', 'another.alias', 'somename']) + mock_vhost.filep = server_conf + mock_vhost.names = names + mock_vhost.path = [] + self.assertRaises(errors.MisconfigurationError, + nparser.add_server_directives, + mock_vhost, + [['foo', 'bar'], + ['ssl_certificate', '/etc/ssl/cert2.pem']], + replace=False) def test_replace_server_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) 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( - filep, target, [['server_name', 'foobar.com']], replace=True) + mock_vhost, [['server_name', 'foobar.com']], replace=True) from certbot_nginx.parser import COMMENT self.assertEqual( nparser.parsed[filep], @@ -182,9 +212,10 @@ class NginxParserTest(util.NginxTest): ['server_name', 'foobar.com'], ['#', COMMENT], ['server_name', 'example.*'], [] ]]]) + mock_vhost.names = set(['foobar.com', 'example.*']) self.assertRaises(errors.MisconfigurationError, nparser.add_server_directives, - filep, set(['foobar.com', 'example.*']), + mock_vhost, [['ssl_certificate', 'cert.pem']], replace=True) @@ -241,8 +272,11 @@ class NginxParserTest(util.NginxTest): def test_get_all_certs_keys(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) filep = nparser.abs_path('sites-enabled/example.com') - nparser.add_server_directives(filep, - set(['.example.com', 'example.*']), + mock_vhost = obj.VirtualHost(filep, + None, None, None, + set(['.example.com', 'example.*']), + None, [0]) + nparser.add_server_directives(mock_vhost, [['ssl_certificate', 'foo.pem'], ['ssl_certificate_key', 'bar.key'], ['listen', '443 ssl']], diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index a92caf788..283e326e9 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -31,7 +31,7 @@ class TlsSniPerformTest(util.NginxTest): token="\xba\xa9\xda? Date: Mon, 26 Sep 2016 16:44:27 -0700 Subject: [PATCH 179/331] Update CONTRIBUTING.md to be more welcoming. (#3540) --- CONTRIBUTING.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f1625658..d740b7d89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,4 +15,21 @@ to the Sphinx generated docs is provided below. --> -https://certbot.eff.org/docs/contributing.html +# Certbot Contributing Guide + +Hi! Welcome to the Certbot project. We look forward to collaborating with you. + +If you're reporting a bug in Certbot, please make sure to include: + - The version of Certbot you're running. + - The operating system you're running it on. + - The commands you ran. + - What you expected to happen, and + - What actually happened. + +If you're a developer, we have some helpful information in our +[Developer's Guide](https://certbot.eff.org/docs/contributing.html) to get you +started. In particular, we recommend you read these sections + + - [Finding issues to work on](https://certbot.eff.org/docs/contributing.html#find-issues-to-work-on) + - [Coding style](https://certbot.eff.org/docs/contributing.html#coding-style) + - [Submitting a pull request](https://certbot.eff.org/docs/contributing.html#submitting-a-pull-request) From 4358c81f068501076057d1b071263290e5dc45ac Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 27 Sep 2016 12:08:32 -0700 Subject: [PATCH 180/331] Improve CHANGES.rst. (#3541) Link to a more accurate / useful GitHub page. Partial fix for #3420. * Improve CHANGES.rst. --- CHANGES.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4ce41a8bc..3b92c125f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,11 +1,8 @@ ChangeLog ========= -Please note: -the change log will only get updated after first release - for now please use the -`commit log `_. +To see the changes in a given release, view the issues closed in a given +release's GitHub milestone: -To see the changes in a given release, inspect the github milestone for the -release. For instance: - -https://github.com/certbot/certbot/issues?utf8=%E2%9C%93&q=milestone%3A0.3.0 + - `Past releases `_ + - `Upcoming releases `_ From b65ea31b42fe419102c08b914c075ba1d1958dfb Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Wed, 28 Sep 2016 20:42:00 +0300 Subject: [PATCH 181/331] Add rope directory to gitignore (#3554) * Ignore .ropeproject --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 48ec7910b..ac2842e27 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ tags *.sw? \#*# .idea +.ropeproject # auth --cert-path --chain-path /*.pem From 769ebfce5ef6059c35a75f2bd63d1419be41e924 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 28 Sep 2016 15:52:08 -0700 Subject: [PATCH 182/331] Remove pointless question (#3526) * remove unhelpful question about servernames and default vhosts * add prefix about names found in config files * test we include configuration files prefix * Tell the user what kind of conf files were missing domains * Revert "Tell the user what kind of conf files were missing domains" This reverts commit 1066a88daec7d995e2163aa8bcad456846ae8637. --- certbot/display/ops.py | 25 +++++++++++-------------- certbot/tests/display/ops_test.py | 10 ++-------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index e8520fe96..ee570221f 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -103,18 +103,8 @@ def choose_names(installer): names = get_valid_domains(domains) if not names: - manual = z_util(interfaces.IDisplay).yesno( - "No names were found in your configuration files.{0}You should " - "specify ServerNames in your config files in order to allow for " - "accurate installation of your certificate.{0}" - "If you do use the default vhost, you may specify the name " - "manually. Would you like to continue?{0}".format(os.linesep), - default=True) - - if manual: - return _choose_names_manually() - else: - return [] + return _choose_names_manually( + "No names were found in your configuration files. ") code, names = _filter_names(names) if code == display_util.OK and names: @@ -157,10 +147,17 @@ def _filter_names(names): return code, [str(s) for s in names] -def _choose_names_manually(): - """Manually input names for those without an installer.""" +def _choose_names_manually(prompt_prefix=""): + """Manually input names for those without an installer. + :param str prompt_prefix: string to prepend to prompt for domains + + :returns: list of provided names + :rtype: `list` of `str` + + """ code, input_ = z_util(interfaces.IDisplay).input( + prompt_prefix + "Please enter in your domain name(s) (comma and/or space separated) ", cli_flag="--domains") diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 26f67b69f..2e3e65261 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -206,20 +206,14 @@ class ChooseNamesTest(unittest.TestCase): @mock.patch("certbot.display.ops.z_util") def test_no_names_choose(self, mock_util): self.mock_install().get_all_names.return_value = set() - mock_util().yesno.return_value = True domain = "example.com" mock_util().input.return_value = (display_util.OK, domain) actual_doms = self._call(self.mock_install) self.assertEqual(mock_util().input.call_count, 1) self.assertEqual(actual_doms, [domain]) - - @mock.patch("certbot.display.ops.z_util") - def test_no_names_quit(self, mock_util): - self.mock_install().get_all_names.return_value = set() - mock_util().yesno.return_value = False - - self.assertEqual(self._call(self.mock_install), []) + self.assertTrue( + "configuration files" in mock_util().input.call_args[0][0]) @mock.patch("certbot.display.ops.z_util") def test_filter_names_valid_return(self, mock_util): From 5fda61f2714f1f1dd4baf4486c08d08010a4262d Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 29 Sep 2016 15:31:13 -0700 Subject: [PATCH 183/331] Allow validation of cross-domain redirects (#3561) * Update compatibility validator to pass redirect check when redirecting to a different domain, whether http or https. --- .../certbot_compatibility_test/validator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/validator.py b/certbot-compatibility-test/certbot_compatibility_test/validator.py index e82b2c049..333b47296 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/validator.py +++ b/certbot-compatibility-test/certbot_compatibility_test/validator.py @@ -40,8 +40,15 @@ class Validator(object): return False redirect_location = response.headers.get("location", "") + # We're checking that the redirect we added behaves correctly. + # It's okay for some server configuration to redirect to an + # http URL, as long as it's on some other domain. if not redirect_location.startswith("https://"): - return False + if not redirect_location.startswith("http://"): + return False + else: + if redirect_location[len("http://"):] == name: + return False if response.status_code != 301: logger.error("Server did not redirect with permanent code") From c9bc0345129f8e927d28a59845f4504974fb01b2 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 29 Sep 2016 16:16:07 -0700 Subject: [PATCH 184/331] Update Nginx redirect enhancement process to modify appropriate blocks (#3546) * Cache the vhost we find during nginx deployment for OCSP enhancement. * Refactor to pass domain into enhancement functions * Add https redirect to most name-matching block listening non-sslishly. * Redirect enhancement chooses the vhost most closely matching target_name that is listening to port 80 without using ssl. * Add default listen 80 directive when it is implicitly defined --- certbot-nginx/certbot_nginx/configurator.py | 170 +++++++++++++++--- certbot-nginx/certbot_nginx/parser.py | 18 ++ .../certbot_nginx/tests/configurator_test.py | 55 +++++- .../certbot_nginx/tests/parser_test.py | 32 +++- .../etc_nginx/sites-enabled/migration.com | 19 ++ 5 files changed, 259 insertions(+), 35 deletions(-) create mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/migration.com diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 0a23f1f07..bfc7b6a67 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -58,6 +58,8 @@ class NginxConfigurator(common.Plugin): hidden = True + DEFAULT_LISTEN_PORT = '80' + @classmethod def add_parser_arguments(cls, add): add("server-root", default=constants.CLI_DEFAULTS["server_root"], @@ -196,19 +198,13 @@ class NginxConfigurator(common.Plugin): vhost = None matches = self._get_ranked_matches(target_name) - if not matches: + vhost = self._select_best_name_match(matches) + if not vhost: # No matches. Raise a misconfiguration error. raise errors.MisconfigurationError( "Cannot find a VirtualHost matching domain %s." % (target_name)) - elif matches[0]['rank'] in xrange(2, 6): - # Wildcard match - need to find the longest one - rank = matches[0]['rank'] - wildcards = [x for x in matches if x['rank'] == rank] - vhost = max(wildcards, key=lambda x: len(x['name']))['vhost'] else: - vhost = matches[0]['vhost'] - - if vhost is not None: + # Note: if we are enhancing with ocsp, vhost should already be ssl. if not vhost.ssl: self._make_server_ssl(vhost) @@ -223,6 +219,41 @@ class NginxConfigurator(common.Plugin): the numerical rank :rtype: list + """ + vhost_list = self.parser.get_vhosts() + return self._rank_matches_by_name_and_ssl(vhost_list, target_name) + + def _select_best_name_match(self, matches): + """Returns the best name match of a ranked list of vhosts. + + :param list matches: list of dicts containing the vhost, the matching name, + and the numerical rank + :returns: the most matching vhost + :rtype: :class:`~certbot_nginx.obj.VirtualHost` + + """ + if not matches: + return None + elif matches[0]['rank'] in xrange(2, 6): + # Wildcard match - need to find the longest one + rank = matches[0]['rank'] + wildcards = [x for x in matches if x['rank'] == rank] + return max(wildcards, key=lambda x: len(x['name']))['vhost'] + else: + # Exact or regex match + return matches[0]['vhost'] + + + def _rank_matches_by_name_and_ssl(self, vhost_list, target_name): + """Returns a ranked list of vhosts from vhost_list that match target_name. + The ranking gives preference to SSL vhosts. + + :param list vhost_list: list of vhosts to filter and rank + :param str target_name: The name to match + :returns: list of dicts containing the vhost, the matching name, and + the numerical rank + :rtype: list + """ # Nginx chooses a matching server name for a request with precedence: # 1. exact name match @@ -230,7 +261,7 @@ class NginxConfigurator(common.Plugin): # 3. longest wildcard name ending with * # 4. first matching regex in order of appearance in the file matches = [] - for vhost in self.parser.get_vhosts(): + for vhost in vhost_list: name_type, name = parser.get_best_match(target_name, vhost.names) if name_type == 'exact': matches.append({'vhost': vhost, @@ -250,6 +281,73 @@ class NginxConfigurator(common.Plugin): 'rank': 6 if vhost.ssl else 7}) return sorted(matches, key=lambda x: x['rank']) + + def choose_redirect_vhost(self, target_name, port): + """Chooses a single virtual host for redirect enhancement. + + Chooses the vhost most closely matching target_name that is + listening to port without using ssl. + + .. todo:: This should maybe return list if no obvious answer + is presented. + + .. todo:: The special name "$hostname" corresponds to the machine's + hostname. Currently we just ignore this. + + :param str target_name: domain name + :param str port: port number + :returns: vhost associated with name + :rtype: :class:`~certbot_nginx.obj.VirtualHost` + + """ + matches = self._get_redirect_ranked_matches(target_name, port) + return self._select_best_name_match(matches) + + def _get_redirect_ranked_matches(self, target_name, port): + """Gets a ranked list of plaintextish port-listening vhosts matching target_name + + Filter all hosts for those listening on port without using ssl. + Rank by how well these match target_name. + + :param str target_name: The name to match + :param str port: port number + :returns: list of dicts containing the vhost, the matching name, and + the numerical rank + :rtype: list + + """ + all_vhosts = self.parser.get_vhosts() + def _port_matches(test_port, matching_port): + # test_port is a number, matching is a number or "" or None + if matching_port == "" or matching_port is None: + # if no port is specified, Nginx defaults to listening on port 80. + return test_port == self.DEFAULT_LISTEN_PORT + else: + return test_port == matching_port + + def _vhost_matches(vhost, port): + found_matching_port = False + if len(vhost.addrs) == 0: + # if there are no listen directives at all, Nginx defaults to + # listening on port 80. + found_matching_port = (port == self.DEFAULT_LISTEN_PORT) + else: + for addr in vhost.addrs: + if _port_matches(port, addr.get_port()) and addr.ssl == False: + found_matching_port = True + + if found_matching_port: + # make sure we don't have an 'ssl on' directive + return not self.parser.has_ssl_on_directive(vhost) + else: + return False + + matching_vhosts = [vhost for vhost in all_vhosts if _vhost_matches(vhost, port)] + + # We can use this ranking function because sslishness doesn't matter to us, and + # there shouldn't be conflicting plaintextish servers listening on 80. + return self._rank_matches_by_name_and_ssl(matching_vhosts, target_name) + def get_all_names(self): """Returns all names found in the Nginx Configuration. @@ -325,6 +423,12 @@ class NginxConfigurator(common.Plugin): :type vhost: :class:`~certbot_nginx.obj.VirtualHost` """ + # If the vhost was implicitly listening on the default Nginx port, + # 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) + snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() # the options file doesn't have a newline at the beginning, but there @@ -370,8 +474,7 @@ class NginxConfigurator(common.Plugin): """ try: - return self._enhance_func[enhancement]( - self.choose_vhost(domain), options) + return self._enhance_func[enhancement](domain, options) except (KeyError, ValueError): raise errors.PluginError( "Unsupported enhancement: {0}".format(enhancement)) @@ -379,38 +482,49 @@ class NginxConfigurator(common.Plugin): logger.warning("Failed %s for %s", enhancement, domain) raise - def _enable_redirect(self, vhost, unused_options): + def _enable_redirect(self, domain, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. Add rewrite directive to non https traffic .. note:: This function saves the configuration - :param vhost: Destination of traffic, an ssl enabled vhost - :type vhost: :class:`~certbot_nginx.obj.VirtualHost` - + :param str domain: domain to enable redirect for :param unused_options: Not currently used :type unused_options: Not Available """ - redirect_block = [[ - ['\n ', 'if', ' ', '($scheme != "https") '], - [['\n ', 'return', ' ', '301 https://$host$request_uri'], - '\n '] - ], ['\n']] - self.parser.add_server_directives( - vhost, redirect_block, replace=False) - logger.info("Redirecting all traffic to ssl in %s", vhost.filep) - def _enable_ocsp_stapling(self, vhost, chain_path): + port = self.DEFAULT_LISTEN_PORT + vhost = None + # If there are blocks listening plaintextishly on self.DEFAULT_LISTEN_PORT, + # choose the most name-matching one. + vhost = self.choose_redirect_vhost(domain, port) + + if vhost is None: + logger.info("No matching insecure server blocks listening on port %s found.", + self.DEFAULT_LISTEN_PORT) + else: + # Redirect plaintextish host to https + redirect_block = [[ + ['\n ', 'if', ' ', '($scheme != "https") '], + [['\n ', 'return', ' ', '301 https://$host$request_uri'], + '\n '] + ], ['\n']] + + self.parser.add_server_directives( + vhost, redirect_block, replace=False) + logger.info("Redirecting all traffic on port %s to ssl in %s", + self.DEFAULT_LISTEN_PORT, vhost.filep) + + def _enable_ocsp_stapling(self, domain, chain_path): """Include OCSP response in TLS handshake - :param vhost: Destination of traffic, an ssl enabled vhost - :type vhost: :class:`~certbot_nginx.obj.VirtualHost` - + :param str domain: domain to enable OCSP response for :param chain_path: chain file path :type chain_path: `str` or `None` """ + vhost = self.choose_vhost(domain) if self.version < (1, 3, 7): raise errors.PluginError("Version 1.3.7 or greater of nginx " "is needed to enable OCSP stapling") diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 13bb38359..a9ef21f2e 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -241,6 +241,24 @@ class NginxParser(object): except IOError: logger.error("Could not open file for writing: %s", filename) + def has_ssl_on_directive(self, vhost): + """Does vhost have ssl on for all ports? + + :param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost in question + + :returns: True if 'ssl on' directive is included + :rtype: bool + + """ + server = vhost.raw + for directive in server: + if not directive or len(directive) < 2: + continue + elif directive[0] == 'ssl' and directive[1] == 'on': + return True + + return False + def add_server_directives(self, vhost, directives, replace): """Add or replace directives in the server block identified by vhost. diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 9bb8a46d8..10f5e5514 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -40,7 +40,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_prepare(self): self.assertEqual((1, 6, 2), self.config.version) - self.assertEqual(5, len(self.config.parser.parsed)) + self.assertEqual(6, len(self.config.parser.parsed)) # ensure we successfully parsed a file for ssl_options self.assertTrue(self.config.parser.loc["ssl_options"]) @@ -67,8 +67,8 @@ class NginxConfiguratorTest(util.NginxTest): mock_gethostbyaddr.return_value = ('155.225.50.69.nephoscale.net', [], []) names = self.config.get_all_names() self.assertEqual(names, set( - ["155.225.50.69.nephoscale.net", - "www.example.org", "another.alias"])) + ["155.225.50.69.nephoscale.net", "www.example.org", "another.alias", + "migration.com", "summer.com", "geese.com"])) def test_supported_enhancements(self): self.assertEqual(['redirect', 'staple-ocsp'], @@ -214,9 +214,34 @@ class NginxConfiguratorTest(util.NginxTest): ], 2)) + def test_deploy_cert_add_explicit_listen(self): + migration_conf = self.config.parser.abs_path('sites-enabled/migration.com') + self.config.deploy_cert( + "summer.com", + "summer/cert.pem", + "summer/key.pem", + "summer/chain.pem", + "summer/fullchain.pem") + self.config.save() + self.config.parser.load() + parsed_migration_conf = util.filter_comments(self.config.parser.parsed[migration_conf]) + self.assertEqual([['server'], + [ + ['server_name', 'migration.com'], + ['server_name', 'summer.com'], + + ['listen', '80'], + ['listen', '5001 ssl'], + ['ssl_certificate', 'summer/fullchain.pem'], + ['ssl_certificate_key', 'summer/key.pem']] + + util.filter_comments(self.config.parser.loc["ssl_options"]) + ], + parsed_migration_conf[0]) + def test_get_all_certs_keys(self): nginx_conf = self.config.parser.abs_path('nginx.conf') example_conf = self.config.parser.abs_path('sites-enabled/example.com') + migration_conf = self.config.parser.abs_path('sites-enabled/migration.com') # Get the default SSL vhost self.config.deploy_cert( @@ -231,12 +256,19 @@ class NginxConfiguratorTest(util.NginxTest): "/etc/nginx/key.pem", "/etc/nginx/chain.pem", "/etc/nginx/fullchain.pem") + self.config.deploy_cert( + "migration.com", + "migration/cert.pem", + "migration/key.pem", + "migration/chain.pem", + "migration/fullchain.pem") self.config.save() self.config.parser.load() self.assertEqual(set([ ('example/fullchain.pem', 'example/key.pem', example_conf), ('/etc/nginx/fullchain.pem', '/etc/nginx/key.pem', nginx_conf), + ('migration/fullchain.pem', 'migration/key.pem', migration_conf), ]), self.config.get_all_certs_keys()) @mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform") @@ -390,6 +422,8 @@ class NginxConfiguratorTest(util.NginxTest): OpenSSL.crypto.FILETYPE_PEM, key_file.read()) def test_redirect_enhance(self): + # Test that we successfully add a redirect when there is + # a listen directive expected = [ ['if', '($scheme != "https") '], [['return', '301 https://$host$request_uri']] @@ -401,6 +435,21 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + # Test that we successfully add a redirect when there is + # no listen directive + migration_conf = self.config.parser.abs_path('sites-enabled/migration.com') + self.config.enhance("migration.com", "redirect") + + generated_conf = self.config.parser.parsed[migration_conf] + self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + + def test_redirect_dont_enhance(self): + # Test that we don't accidentally add redirect to ssl-only block + with mock.patch("certbot_nginx.configurator.logger") as mock_logger: + self.config.enhance("geese.com", "redirect") + self.assertEqual(mock_logger.info.call_args[0][0], + 'No matching insecure server blocks listening on port %s found.') + def test_staple_ocsp_bad_version(self): self.config.version = (1, 3, 1) self.assertRaises(errors.PluginError, self.config.enhance, diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 18de59daf..d148e89aa 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -47,7 +47,8 @@ class NginxParserTest(util.NginxTest): self.assertEqual(set([nparser.abs_path(x) for x in ['foo.conf', 'nginx.conf', 'server.conf', 'sites-enabled/default', - 'sites-enabled/example.com']]), + 'sites-enabled/example.com', + 'sites-enabled/migration.com']]), set(nparser.parsed.keys())) self.assertEqual([['server_name', 'somename alias another.alias']], nparser.parsed[nparser.abs_path('server.conf')]) @@ -71,7 +72,7 @@ class NginxParserTest(util.NginxTest): parsed = nparser._parse_files(nparser.abs_path( 'sites-enabled/example.com.test')) self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test')))) - self.assertEqual(2, len( + self.assertEqual(3, len( glob.glob(nparser.abs_path('sites-enabled/*.test')))) self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], @@ -135,7 +136,7 @@ class NginxParserTest(util.NginxTest): '*.www.example.com']), [], [2, 1, 0]) - self.assertEqual(5, len(vhosts)) + self.assertEqual(7, len(vhosts)) example_com = [x for x in vhosts if 'example.com' in x.filep][0] self.assertEqual(vhost3, example_com) default = [x for x in vhosts if 'default' in x.filep][0] @@ -147,6 +148,26 @@ class NginxParserTest(util.NginxTest): somename = [x for x in vhosts if 'somename' in x.names][0] self.assertEqual(vhost2, somename) + def test_has_ssl_on_directive(self): + nparser = parser.NginxParser(self.config_path, self.ssl_options) + mock_vhost = obj.VirtualHost(None, None, None, None, None, + [['listen', 'myhost default_server'], + ['server_name', 'www.example.org'], + [['location', '/'], [['root', 'html'], ['index', 'index.html index.htm']]] + ], None) + self.assertFalse(nparser.has_ssl_on_directive(mock_vhost)) + mock_vhost.raw = [['listen', '*:80 default_server ssl'], + ['server_name', '*.www.foo.com *.www.example.com'], + ['root', '/home/ubuntu/sites/foo/']] + self.assertFalse(nparser.has_ssl_on_directive(mock_vhost)) + mock_vhost.raw = [['listen', '80 ssl'], + ['server_name', '*.www.foo.com *.www.example.com']] + self.assertFalse(nparser.has_ssl_on_directive(mock_vhost)) + mock_vhost.raw = [['listen', '80'], + ['ssl', 'on'], + ['server_name', '*.www.foo.com *.www.example.com']] + self.assertTrue(nparser.has_ssl_on_directive(mock_vhost)) + def test_add_server_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'), @@ -282,7 +303,10 @@ class NginxParserTest(util.NginxTest): ['listen', '443 ssl']], replace=False) c_k = nparser.get_all_certs_keys() - self.assertEqual(set([('foo.pem', 'bar.key', filep)]), c_k) + migration_file = nparser.abs_path('sites-enabled/migration.com') + self.assertEqual(set([('foo.pem', 'bar.key', filep), + ('cert.pem', 'cert.key', migration_file) + ]), c_k) def test_parse_server_ssl(self): server = parser.parse_server([ diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/migration.com b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/migration.com new file mode 100644 index 000000000..17bc6d0c3 --- /dev/null +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/migration.com @@ -0,0 +1,19 @@ +server { + server_name migration.com; + server_name summer.com; +} + +server { + listen 443 ssl; + server_name migration.com; + server_name geese.com; + + ssl_certificate cert.pem; + ssl_certificate_key cert.key; + + ssl_session_cache shared:SSL:1m; + ssl_session_timeout 5m; + + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; +} From 290c11221718be4035cd950509b58efd2cec6add Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 30 Sep 2016 20:27:04 -0700 Subject: [PATCH 185/331] Tweak for Travis performance (#3562) * Tweak for Travis performance - merge cover and py27 BOULDER_INTEGRATION into one matrix entry - re-order to put the fastest environments last, improving average case parallelism * Also put the things most likely to fail at the top --- .travis.yml | 51 +++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5ccf39811..8477bb163 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,20 @@ env: matrix: include: + - python: "2.7" + env: TOXENV=cover BOULDER_INTEGRATION=1 + sudo: true + after_failure: + - sudo cat /var/log/mysql/error.log + - ps aux | grep mysql + - python: "2.7" + env: TOXENV=lint + - python: "2.7" + env: TOXENV=py27-oldest BOULDER_INTEGRATION=1 + sudo: true + after_failure: + - sudo cat /var/log/mysql/error.log + - ps aux | grep mysql - python: "2.6" env: TOXENV=py26 BOULDER_INTEGRATION=1 sudo: true @@ -31,25 +45,11 @@ matrix: after_failure: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql - - python: "2.7" - env: TOXENV=apacheconftest - sudo: required - - python: "2.7" - env: TOXENV=nginxroundtrip - - python: "2.7" - env: TOXENV=py27 BOULDER_INTEGRATION=1 - sudo: true - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - - python: "2.7" - env: TOXENV=py27-oldest BOULDER_INTEGRATION=1 - sudo: true - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - - python: "2.7" - env: TOXENV=lint + - sudo: required + env: TOXENV=nginx_compat + services: docker + before_install: + addons: - sudo: required env: TOXENV=le_auto services: docker @@ -60,19 +60,18 @@ matrix: services: docker before_install: addons: - - sudo: required - env: TOXENV=nginx_compat - services: docker - before_install: - addons: - - python: "2.7" - env: TOXENV=cover - python: "3.3" env: TOXENV=py33 - python: "3.4" env: TOXENV=py34 - python: "3.5" env: TOXENV=py35 + - python: "2.7" + env: TOXENV=apacheconftest + sudo: required + - python: "2.7" + env: TOXENV=nginxroundtrip + # Only build pushes to the master branch, PRs, and branches beginning with # `test-`. This reduces the number of simultaneous Travis runs, which speeds From bde1d9fdb1a3e5bbd1910d8b6bb3c559c31a61bd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 4 Oct 2016 10:18:05 -0700 Subject: [PATCH 186/331] Renew symlink safety (#3560) Re-do the fix for #3497 to ensure it works in all cases. * If lineages are in an inconsistent (non-deployed) state, deploy them * Test new _handle_identical_cert case * Move lineage.has_pending_deployment() check up to _auth_from_domains Less conceptually nice, but in the "renew" verb case it wasn't being called :( * Swap _auth_from_domains return type * It now matches _treat_as_renewal & _handle_identical_cert_request etc * Revert "Move lineage.has_pending_deployment() check up to _auth_from_domains" This reverts commit a7fe734d73a9941fdb7608f6d6f30843a4f43912. * Move test back to handle_identical_cert_request * We need to check for non-deployment on two separate code paths - Once high up in "renew" (because failure to be deployed stops us from divind down the stack) - Once way down in _handle_identical_cert_request (because that's where it makes the most sense for run / certonly) - So refactor that work into storage.py * We don't necessarily reinstall --- certbot/main.py | 25 +++++++++---------------- certbot/renewal.py | 1 + certbot/storage.py | 16 ++++++++++++++++ certbot/tests/cli_test.py | 2 +- certbot/tests/main_test.py | 10 +++------- certbot/tests/storage_test.py | 17 +++++++++++++++++ 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 3557f65b3..dd497d14d 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -67,16 +67,12 @@ def _report_successful_dry_run(config): reporter_util.HIGH_PRIORITY, on_crash=False) - def _auth_from_domains(le_client, config, domains, lineage=None): - """Authenticate and enroll certificate.""" - # Note: This can raise errors... caught above us though. This is now - # a three-way case: reinstall (which results in a no-op here because - # although there is a relevant lineage, we don't do anything to it - # inside this function -- we don't obtain a new certificate), renew - # (which results in treating the request as a renewal), or newcert - # (which results in treating the request as a new certificate request). + """Authenticate and enroll certificate. + :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal + action can be: "newcert" | "renew" | "reinstall" + """ # If lineage is specified, use that one instead of looking around for # a matching one. if lineage is None: @@ -91,7 +87,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None): # The lineage already exists; allow the caller to try installing # it without getting a new certificate at all. logger.info("Keeping the existing certificate") - return lineage, "reinstall" + return "reinstall", lineage hooks.pre_hook(config) try: @@ -110,7 +106,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None): if not config.dry_run and not config.verb == "renew": _report_new_cert(config, lineage.cert, lineage.fullchain) - return lineage, action + return action, lineage def _handle_subset_cert_request(config, domains, cert): @@ -165,10 +161,7 @@ def _handle_identical_cert_request(config, lineage): :rtype: tuple """ - if lineage.has_pending_deployment(): - logger.warn("Found a new cert /archive/ that was not linked to in /live/; " - "fixing and reinstalling..") - lineage.update_all_links_to(lineage.latest_common_version()) + if not lineage.ensure_deployed(): return "reinstall", lineage if renewal.should_renew(config, lineage): return "renew", lineage @@ -515,7 +508,7 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals # TODO: Handle errors from _init_le_client? le_client = _init_le_client(config, authenticator, installer) - lineage, action = _auth_from_domains(le_client, config, domains) + action, lineage = _auth_from_domains(le_client, config, domains) le_client.deploy_certificate( domains, lineage.privkey, lineage.cert, @@ -567,7 +560,7 @@ def obtain_cert(config, plugins, lineage=None): # SHOWTIME: Possibly obtain/renew a cert, and set action to renew | newcert | reinstall if config.csr is None: # the common case domains = _find_domains(config, installer) - _, action = _auth_from_domains(le_client, config, domains, lineage) + action, _ = _auth_from_domains(le_client, config, domains, lineage) else: assert lineage is None, "Did not expect a CSR with a RenewableCert" _csr_obtain_cert(config, le_client) diff --git a/certbot/renewal.py b/certbot/renewal.py index 339d7b7ff..5e57c3e20 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -341,6 +341,7 @@ def renew_all_lineages(config): else: # XXX: ensure that each call here replaces the previous one zope.component.provideUtility(lineage_config) + renewal_candidate.ensure_deployed() if should_renew(lineage_config, renewal_candidate): plugins = plugins_disco.PluginsRegistry.find_all() from certbot import main diff --git a/certbot/storage.py b/certbot/storage.py index 5ca2ff6a9..c740657d8 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -520,6 +520,22 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # for the others. return max(self.newest_available_version(x) for x in ALL_FOUR) + 1 + def ensure_deployed(self): + """Make sure we've deployed the latest version. + + :returns: False if a change was needed, True otherwise + :rtype: bool + + May need to recover from rare interrupted / crashed states.""" + + if self.has_pending_deployment(): + logger.warn("Found a new cert /archive/ that was not linked to in /live/; " + "fixing...") + self.update_all_links_to(self.latest_common_version()) + return False + return True + + def has_pending_deployment(self): """Is there a later version of all of the managed items? diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 6cbbea631..39a54258a 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -177,7 +177,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args = ["--standalone", "certonly", "-m", "none@none.com", "-d", "example.com", '--agree-tos'] + self.standard_args det.return_value = mock.MagicMock(), None - afd.return_value = mock.MagicMock(), "newcert" + afd.return_value = "newcert", mock.MagicMock() with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: self._call_no_clientmock(args) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 69291871a..fab1065c5 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -17,17 +17,13 @@ class MainTest(unittest.TestCase): def tearDown(self): pass - @mock.patch("certbot.main.logger") - def test_handle_identical_cert_request_pending(self, _mock_logger): - # For now, just test has_pending_deployment_branch; other - # coverage is in cli_test.py... + def test_handle_identical_cert_request_pending(self): from certbot import main mock_lineage = mock.Mock() - mock_lineage.has_pending_deployment.return_value = True + mock_lineage.ensure_deployed.return_value = False # pylint: disable=protected-access ret = main._handle_identical_cert_request(mock.Mock(), mock_lineage) self.assertEqual(ret, ("reinstall", mock_lineage)) - self.assertEqual(mock_lineage.update_all_links_to.call_count, 1) class ObtainCertTest(unittest.TestCase): """Tests for certbot.main.obtain_cert.""" @@ -55,7 +51,7 @@ class ObtainCertTest(unittest.TestCase): def test_no_reinstall_text_pause(self, mock_auth): mock_notification = self.mock_get_utility().notification mock_notification.side_effect = self._assert_no_pause - mock_auth.return_value = (mock.ANY, 'reinstall') + mock_auth.return_value = ('reinstall', mock.ANY) self._call('certonly --webroot -d example.com -t'.split()) def _assert_no_pause(self, message, height=42, pause=True): diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 7ac7771da..9566e0aec 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -258,6 +258,23 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.latest_common_version(), 17) self.assertEqual(self.test_rc.next_free_version(), 18) + @mock.patch("certbot.storage.logger") + def test_ensure_deployed(self, mock_logger): + mock_update = self.test_rc.update_all_links_to = mock.Mock() + mock_has_pending = self.test_rc.has_pending_deployment = mock.Mock() + self.test_rc.latest_common_version = mock.Mock() + + mock_has_pending.return_value = False + self.assertEqual(self.test_rc.ensure_deployed(), True) + self.assertEqual(mock_update.call_count, 0) + self.assertEqual(mock_logger.warn.call_count, 0) + + mock_has_pending.return_value = True + self.assertEqual(self.test_rc.ensure_deployed(), False) + self.assertEqual(mock_update.call_count, 1) + self.assertEqual(mock_logger.warn.call_count, 1) + + def test_update_link_to(self): for ver in six.moves.range(1, 6): for kind in ALL_FOUR: From 3ae6c90a6a69135e01f0f6c9f07ac3ca3f006e35 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 4 Oct 2016 12:47:36 -0700 Subject: [PATCH 187/331] The sudo environments take longer to allocate (#3578) One more tiny tweak, placing the slower sudo environments back up the list. I expect this should save us another 10-20 seconds. - there's about a 50 second delay in starting apacheconftest, so move it back up the priority queue of jobs to start --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8477bb163..8101fb3a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,15 +60,15 @@ matrix: services: docker before_install: addons: + - python: "2.7" + env: TOXENV=apacheconftest + sudo: required - python: "3.3" env: TOXENV=py33 - python: "3.4" env: TOXENV=py34 - python: "3.5" env: TOXENV=py35 - - python: "2.7" - env: TOXENV=apacheconftest - sudo: required - python: "2.7" env: TOXENV=nginxroundtrip From 2146ec9535d9b4c94b5176e5aab6cea9b6970029 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 4 Oct 2016 14:48:06 -0700 Subject: [PATCH 188/331] Remove psutil dep (#3579) * Build letsencrypt-auto-source/letsencrypt-auto to bring it up to date * Remove psutil dep from certbot-auto (fixes #3341) --- letsencrypt-auto-source/letsencrypt-auto | 38 +++++++------------ .../pieces/letsencrypt-auto-requirements.txt | 22 ----------- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 5979b0848..a7c98790d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -590,8 +590,9 @@ if [ "$1" = "--le-auto-phase2" ]; then # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" # This is the flattened list of packages certbot-auto installs. To generate -# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and -# then use `hashin` or a more secure method to gather the hashes. +# this, do +# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`, +# and then use `hashin` or a more secure method to gather the hashes. argparse==1.4.0 \ --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ @@ -671,28 +672,6 @@ parsedatetime==2.1 \ pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 -psutil==3.3.0 \ - --hash=sha256:584f0b29fcc5d523b433cb8918b2fc74d67e30ee0b44a95baf031528f424619f \ - --hash=sha256:28ca0b6e9d99aa8dc286e8747a4471362b69812a25291de29b6a8d70a1545a0d \ - --hash=sha256:167ad5fff52a672c4ddc1c1a0b25146d6813ebb08a9aab0a3ac45f8a5b669c3b \ - --hash=sha256:e6dea6173a988727bb223d3497349ad5cdef5c0b282eff2d83e5f9065c53f85f \ - --hash=sha256:2af5e0a4aad66049955d0734aa4e3dc8caa17a9eaf8b4c1a27a5f1ee6e40f6fc \ - --hash=sha256:d9884dc0dc2e55e2448e495778dc9899c1c8bf37aeb2f434c1bea74af93c2683 \ - --hash=sha256:e27c2fe6dfcc8738be3d2c5a022f785eb72971057e1a9e1e34fba73bce8a71a6 \ - --hash=sha256:65afd6fecc8f3aed09ee4be63583bc8eb472f06ceaa4fe24c4d1d5a1a3c0e13f \ - --hash=sha256:ba1c558fbfcdf94515c2394b1155c1dc56e2bc2a9c17d30349827c9ed8a67e46 \ - --hash=sha256:ba95ea0022dcb64d36f0c1335c0605fae35bdf3e0fea8d92f5d0f6456a35e55b \ - --hash=sha256:421b6591d16b509aaa8d8c15821d66bb94cb4a8dc4385cad5c51b85d4a096d85 \ - --hash=sha256:326b305cbdb6f94dafbfe2c26b11da88b0ab07b8a07f8188ab9d75ff0c6e841a \ - --hash=sha256:9aede5b2b6fe46b3748ea8e5214443890d1634027bef3d33b7dad16556830278 \ - --hash=sha256:73bed1db894d1aa9c3c7e611d302cdeab7ae8a0dc0eeaf76727878db1ac5cd87 \ - --hash=sha256:935b5dd6d558af512f42501a7c08f41d7aff139af1bb3959daa3abb859234d6c \ - --hash=sha256:4ca0111cf157dcc0f2f69a323c5b5478718d68d45fc9435d84be0ec0f186215b \ - --hash=sha256:b6f13c95398a3fcf0226c4dcfa448560ba5865259cd96ec2810658651e932189 \ - --hash=sha256:ee6be30d1635bbdea4c4325d507dc8a0dbbde7e1c198bd62ddb9f43198b9e214 \ - --hash=sha256:dfa786858c268d7fbbe1b6175e001ec02738d7cfae0a7ce77bf9b651af676729 \ - --hash=sha256:aa77f9de72af9c16cc288cd4a24cf58824388f57d7a81e400c4616457629870e \ - --hash=sha256:f500093357d04da8140d87932cac2e54ef592a54ca8a743abb2850f60c2c22eb pyasn1==0.1.9 \ --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ @@ -708,6 +687,15 @@ pyasn1==0.1.9 \ pyopenssl==16.0.0 \ --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d +pyparsing==2.1.8 \ + --hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \ + --hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \ + --hash=sha256:ab09aee814c0241ff0c503cff30018219fe1fc14501d89f406f4664a0ec9fbcd \ + --hash=sha256:6e9a7f052f8e26bcf749e4033e3115b6dc7e3c85aafcb794b9a88c9d9ef13c97 \ + --hash=sha256:9f463a6bcc4eeb6c08f1ed84439b17818e2085937c0dee0d7674ac127c67c12b \ + --hash=sha256:3626b4d81cfb300dad57f52f2f791caaf7b06c09b368c0aa7b868e53a5775424 \ + --hash=sha256:367b90cc877b46af56d4580cd0ae278062903f02b8204ab631f5a2c0f50adfd0 \ + --hash=sha256:9f1ea360086cd68681e7f4ca8f1f38df47bf81942a0d76a9673c2d23eff35b13 pyRFC3339==1.0 \ --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 @@ -1001,7 +989,7 @@ else # Print latest released version of LE to stdout: python fetch.py --latest-version - + # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm # in, and make sure its signature verifies: python fetch.py --le-auto-script v1.2.3 diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 342aa2f88..282a8dfe5 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -81,28 +81,6 @@ parsedatetime==2.1 \ pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 -psutil==3.3.0 \ - --hash=sha256:584f0b29fcc5d523b433cb8918b2fc74d67e30ee0b44a95baf031528f424619f \ - --hash=sha256:28ca0b6e9d99aa8dc286e8747a4471362b69812a25291de29b6a8d70a1545a0d \ - --hash=sha256:167ad5fff52a672c4ddc1c1a0b25146d6813ebb08a9aab0a3ac45f8a5b669c3b \ - --hash=sha256:e6dea6173a988727bb223d3497349ad5cdef5c0b282eff2d83e5f9065c53f85f \ - --hash=sha256:2af5e0a4aad66049955d0734aa4e3dc8caa17a9eaf8b4c1a27a5f1ee6e40f6fc \ - --hash=sha256:d9884dc0dc2e55e2448e495778dc9899c1c8bf37aeb2f434c1bea74af93c2683 \ - --hash=sha256:e27c2fe6dfcc8738be3d2c5a022f785eb72971057e1a9e1e34fba73bce8a71a6 \ - --hash=sha256:65afd6fecc8f3aed09ee4be63583bc8eb472f06ceaa4fe24c4d1d5a1a3c0e13f \ - --hash=sha256:ba1c558fbfcdf94515c2394b1155c1dc56e2bc2a9c17d30349827c9ed8a67e46 \ - --hash=sha256:ba95ea0022dcb64d36f0c1335c0605fae35bdf3e0fea8d92f5d0f6456a35e55b \ - --hash=sha256:421b6591d16b509aaa8d8c15821d66bb94cb4a8dc4385cad5c51b85d4a096d85 \ - --hash=sha256:326b305cbdb6f94dafbfe2c26b11da88b0ab07b8a07f8188ab9d75ff0c6e841a \ - --hash=sha256:9aede5b2b6fe46b3748ea8e5214443890d1634027bef3d33b7dad16556830278 \ - --hash=sha256:73bed1db894d1aa9c3c7e611d302cdeab7ae8a0dc0eeaf76727878db1ac5cd87 \ - --hash=sha256:935b5dd6d558af512f42501a7c08f41d7aff139af1bb3959daa3abb859234d6c \ - --hash=sha256:4ca0111cf157dcc0f2f69a323c5b5478718d68d45fc9435d84be0ec0f186215b \ - --hash=sha256:b6f13c95398a3fcf0226c4dcfa448560ba5865259cd96ec2810658651e932189 \ - --hash=sha256:ee6be30d1635bbdea4c4325d507dc8a0dbbde7e1c198bd62ddb9f43198b9e214 \ - --hash=sha256:dfa786858c268d7fbbe1b6175e001ec02738d7cfae0a7ce77bf9b651af676729 \ - --hash=sha256:aa77f9de72af9c16cc288cd4a24cf58824388f57d7a81e400c4616457629870e \ - --hash=sha256:f500093357d04da8140d87932cac2e54ef592a54ca8a743abb2850f60c2c22eb pyasn1==0.1.9 \ --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ From 3c85ecbfeeae2000e804338528d7d28dd43d6069 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 4 Oct 2016 16:45:24 -0700 Subject: [PATCH 189/331] Test farm test fixes (#3582) --- tests/letstest/scripts/test_apache2.sh | 1 + tests/letstest/scripts/test_leauto_upgrades.sh | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/letstest/scripts/test_apache2.sh b/tests/letstest/scripts/test_apache2.sh index 3e0846216..7fa860302 100755 --- a/tests/letstest/scripts/test_apache2.sh +++ b/tests/letstest/scripts/test_apache2.sh @@ -17,6 +17,7 @@ then CONFFILE=/etc/httpd/conf/httpd.conf sudo setenforce 0 || true #disable selinux sudo yum -y install httpd + sudo yum -y install nghttp2 || echo this is probably ok but see https://bugzilla.redhat.com/show_bug.cgi?id=1358875 sudo service httpd start sudo mkdir -p /var/www/$PUBLIC_HOSTNAME/public_html sudo chmod -R oug+rwx /var/www diff --git a/tests/letstest/scripts/test_leauto_upgrades.sh b/tests/letstest/scripts/test_leauto_upgrades.sh index 0ad0d6081..08247fe8d 100755 --- a/tests/letstest/scripts/test_leauto_upgrades.sh +++ b/tests/letstest/scripts/test_leauto_upgrades.sh @@ -22,14 +22,14 @@ if ! command -v git ; then fi fi BRANCH=`git rev-parse --abbrev-ref HEAD` -git checkout v0.1.0 +git checkout -f v0.1.0 ./letsencrypt-auto -v --debug --version unset PIP_INDEX_URL export PIP_EXTRA_INDEX_URL="$SAVE" git checkout -f "$BRANCH" -if ! ./letsencrypt-auto -v --debug --version | grep 0.3.0 ; then +if ! ./letsencrypt-auto -v --debug --version | grep 0.9.0 ; then echo upgrade appeared to fail exit 1 fi From da22e645638ab8d9f3334555086694f86a7892f9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 4 Oct 2016 16:49:51 -0700 Subject: [PATCH 190/331] Allow tests to pass without dnspython (#3581) * move skipUnless to test_util * add skip_unless to acme test_util * Make dns_resolver_tests work with and without dnspython * make acme.challenges_test pass when dns is unavailable --- acme/acme/challenges_test.py | 63 ++++++++++++++++++++++------------ acme/acme/dns_resolver_test.py | 55 +++++++++++++++++++++-------- acme/acme/test_util.py | 22 ++++++++++++ certbot/plugins/util_test.py | 26 +++----------- certbot/tests/test_util.py | 22 ++++++++++++ 5 files changed, 131 insertions(+), 57 deletions(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 27976931a..dfd40ebdb 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -11,7 +11,6 @@ from acme import errors from acme import jose from acme import test_util - CERT = test_util.load_comparable_cert('cert.pem') KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) @@ -77,6 +76,20 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase): self.assertFalse(response.verify(self.chall, KEY.public_key())) +def dns_available(): + """Checks if dns can be imported. + + :rtype: bool + :returns: ``True`` if dns can be imported, otherwise, ``False`` + + """ + try: + import dns # pylint: disable=unused-variable + except ImportError: # pragma: no cover + return False + return True # pragma: no cover + + class DNS01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes @@ -92,6 +105,7 @@ class DNS01ResponseTest(unittest.TestCase): from acme.challenges import DNS01 self.chall = DNS01(token=(b'x' * 16)) self.response = self.chall.response(KEY) + self.records_for_name_path = "acme.dns_resolver.txt_records_for_name" def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -108,28 +122,35 @@ class DNS01ResponseTest(unittest.TestCase): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) - @mock.patch("acme.dns_resolver.txt_records_for_name") - def test_simple_verify_good_validation(self, mock_resolver): - mock_resolver.return_value = [self.chall.validation(KEY.public_key())] - self.assertTrue(self.response.simple_verify( - self.chall, "local", KEY.public_key())) - mock_resolver.assert_called_once_with( - self.chall.validation_domain_name("local")) + @test_util.skip_unless(dns_available(), + "optional dependency dnspython is not available") + def test_simple_verify_good_validation(self): # pragma: no cover + with mock.patch(self.records_for_name_path) as mock_resolver: + mock_resolver.return_value = [ + self.chall.validation(KEY.public_key())] + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_resolver.assert_called_once_with( + self.chall.validation_domain_name("local")) - @mock.patch("acme.dns_resolver.txt_records_for_name") - def test_simple_verify_good_validation_multiple_txts(self, mock_resolver): - mock_resolver.return_value = [ - "!", self.chall.validation(KEY.public_key())] - self.assertTrue(self.response.simple_verify( - self.chall, "local", KEY.public_key())) - mock_resolver.assert_called_once_with( - self.chall.validation_domain_name("local")) + @test_util.skip_unless(dns_available(), + "optional dependency dnspython is not available") + def test_simple_verify_good_validation_multitxts(self): # pragma: no cover + with mock.patch(self.records_for_name_path) as mock_resolver: + mock_resolver.return_value = [ + "!", self.chall.validation(KEY.public_key())] + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_resolver.assert_called_once_with( + self.chall.validation_domain_name("local")) - @mock.patch("acme.dns_resolver.txt_records_for_name") - def test_simple_verify_bad_validation(self, mock_dns): - mock_dns.return_value = ["!"] - self.assertFalse(self.response.simple_verify( - self.chall, "local", KEY.public_key())) + @test_util.skip_unless(dns_available(), + "optional dependency dnspython is not available") + def test_simple_verify_bad_validation(self): # pragma: no cover + with mock.patch(self.records_for_name_path) as mock_resolver: + mock_resolver.return_value = ["!"] + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) class DNS01Test(unittest.TestCase): diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 53fc0cc77..03f1b3a93 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,13 +1,17 @@ """Tests for acme.dns_resolver.""" +import sys import unittest + import mock -from acme import dns_resolver +from acme import test_util + try: import dns + DNS_AVAILABLE = True # pragma: no cover except ImportError: # pragma: no cover - dns = None + DNS_AVAILABLE = False def create_txt_response(name, txt_records): @@ -21,33 +25,56 @@ def create_txt_response(name, txt_records): return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) -class TxtRecordsForNameTest(unittest.TestCase): +@test_util.skip_unless(DNS_AVAILABLE, + "optional dependency dnspython is not available") +class DnsResolverTestWithDns(unittest.TestCase): + """Tests for acme.dns_resolver when dns is available.""" + @classmethod + def _call(cls, name): + from acme import dns_resolver + return dns_resolver.txt_records_for_name(name) @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_with_single_response(self, mock_dns): mock_dns.return_value = create_txt_response('name', ['response']) - self.assertEqual(['response'], - dns_resolver.txt_records_for_name('name')) + self.assertEqual(['response'], self._call('name')) @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_with_multiple_responses(self, mock_dns): mock_dns.return_value = create_txt_response( 'name', ['response1', 'response2']) - self.assertEqual(['response1', 'response2'], - dns_resolver.txt_records_for_name('name')) + self.assertEqual(['response1', 'response2'], self._call('name')) @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_domain_not_found(self, mock_dns): mock_dns.side_effect = dns.resolver.NXDOMAIN - self.assertEquals([], dns_resolver.txt_records_for_name('name')) + self.assertEquals([], self._call('name')) @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_domain_other_error(self, mock_dns): mock_dns.side_effect = dns.exception.DNSException - self.assertEquals([], dns_resolver.txt_records_for_name('name')) + self.assertEquals([], self._call('name')) - def run(self, result=None): - if dns is None: # pragma: no cover - print(self, "... SKIPPING, no dnspython available") - return - super(TxtRecordsForNameTest, self).run(result) + +class DnsResolverTestWithoutDns(unittest.TestCase): + """Tests for acme.dns_resolver when dns is unavailable.""" + def setUp(self): + self.dns_module = sys.modules['dns'] if 'dns' in sys.modules else None + + if DNS_AVAILABLE: + sys.modules['dns'] = None # pragma: no cover + + def tearDown(self): + if self.dns_module is not None: + sys.modules['dns'] = self.dns_module # pragma: no cover + + @classmethod + def _import_dns(cls): + import dns as failed_dns_import # pylint: disable=unused-variable + + def test_import_error_is_raised(self): + self.assertRaises(ImportError, self._import_dns) + + +if __name__ == '__main__': + unittest.main() # pragma: no cover diff --git a/acme/acme/test_util.py b/acme/acme/test_util.py index 24eceff5a..0f5763682 100644 --- a/acme/acme/test_util.py +++ b/acme/acme/test_util.py @@ -5,6 +5,7 @@ """ import os import pkg_resources +import unittest from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization @@ -73,3 +74,24 @@ def load_pyopenssl_private_key(*names): loader = _guess_loader( names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) + + +def skip_unless(condition, reason): # pragma: no cover + """Skip tests unless a condition holds. + + This implements the basic functionality of unittest.skipUnless + which is only available on Python 2.7+. + + :param bool condition: If ``False``, the test will be skipped + :param str reason: the reason for skipping the test + + :rtype: callable + :returns: decorator that hides tests unless condition is ``True`` + + """ + if hasattr(unittest, "skipUnless"): + return unittest.skipUnless(condition, reason) + elif condition: + return lambda cls: cls + else: + return lambda cls: None diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index 27ede6533..71fb2a023 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -6,6 +6,8 @@ import sys import mock from six.moves import reload_module # pylint: disable=import-error +from certbot.tests import test_util + class PathSurgeryTest(unittest.TestCase): """Tests for certbot.plugins.path_surgery.""" @@ -89,28 +91,8 @@ def psutil_available(): return True -def skipUnless(condition, reason): - """Skip tests unless a condition holds. - - This implements the basic functionality of unittest.skipUnless - which is only available on Python 2.7+. - - :param bool condition: If ``False``, the test will be skipped - :param str reason: the reason for skipping the test - - :rtype: callable - :returns: decorator that hides tests unless condition is ``True`` - - """ - if hasattr(unittest, "skipUnless"): - return unittest.skipUnless(condition, reason) - elif condition: - return lambda cls: cls - else: - return lambda cls: None - - -@skipUnless(psutil_available(), "optional dependency psutil is not available") +@test_util.skip_unless(psutil_available(), + "optional dependency psutil is not available") class AlreadyListeningTestPsutil(unittest.TestCase): """Tests for certbot.plugins.already_listening.""" def _call(self, *args, **kwargs): diff --git a/certbot/tests/test_util.py b/certbot/tests/test_util.py index 24eceff5a..0f5763682 100644 --- a/certbot/tests/test_util.py +++ b/certbot/tests/test_util.py @@ -5,6 +5,7 @@ """ import os import pkg_resources +import unittest from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization @@ -73,3 +74,24 @@ def load_pyopenssl_private_key(*names): loader = _guess_loader( names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) + + +def skip_unless(condition, reason): # pragma: no cover + """Skip tests unless a condition holds. + + This implements the basic functionality of unittest.skipUnless + which is only available on Python 2.7+. + + :param bool condition: If ``False``, the test will be skipped + :param str reason: the reason for skipping the test + + :rtype: callable + :returns: decorator that hides tests unless condition is ``True`` + + """ + if hasattr(unittest, "skipUnless"): + return unittest.skipUnless(condition, reason) + elif condition: + return lambda cls: cls + else: + return lambda cls: None From 76a92d4cdea43d4d943073c64bd7c25a3cb297e3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 5 Oct 2016 10:13:28 -0700 Subject: [PATCH 191/331] Release Certbot 0.9.0 (#3583) * Release 0.9.0 * Bump version to 0.10.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 167 ++++++++++-------- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 132 ++++++++------ letsencrypt-auto | 167 ++++++++++-------- letsencrypt-auto-source/certbot-auto.asc | 14 +- letsencrypt-auto-source/letsencrypt-auto | 23 +-- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 21 ++- 12 files changed, 303 insertions(+), 231 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 94f78d4cd..2b32f7e28 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.10.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index e3dbe4563..2b4ac8563 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.10.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 80f39cf59..27fcde319 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.1" +LE_AUTO_VERSION="0.9.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -34,6 +34,7 @@ Help for certbot itself cannot be provided until it is installed. -n, --non-interactive, --noninteractive run without asking for user input --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + -q, --quiet provide only update/error output -v, --verbose provide more output All arguments are accepted and forwarded to the Certbot client when run." @@ -52,15 +53,19 @@ for arg in "$@" ; do HELP=1;; --noninteractive|--non-interactive) ASSUME_YES=1;; + --quiet) + QUIET=1;; --verbose) VERBOSE=1;; -[!-]*) - while getopts ":hnv" short_arg $arg; do + while getopts ":hnvq" short_arg $arg; do case "$short_arg" in h) HELP=1;; n) ASSUME_YES=1;; + q) + QUIET=1;; v) VERBOSE=1;; esac @@ -121,7 +126,7 @@ ExperimentalBootstrap() { $2 fi else - echo "WARNING: $1 support is very experimental at present..." + echo "FATAL: $1 support is very experimental at present..." echo "if you would like to work on improving it, please ensure you have backups" echo "and then run this script again with the --debug flag!" exit 1 @@ -276,6 +281,30 @@ BootstrapRpmCommon() { exit 1 fi + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then + echo "To use Certbot, packages from the EPEL repository need to be installed." + if ! $SUDO $tool list epel-release >/dev/null 2>&1; then + echo "Please enable this repository and try running Certbot again." + exit 1 + fi + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s + fi + if ! $SUDO $tool install $yes_flag epel-release; then + echo "Could not enable EPEL. Aborting bootstrap!" + exit 1 + fi + fi + pkgs=" gcc dialog @@ -313,10 +342,6 @@ BootstrapRpmCommon() { " fi - if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" - fi - if ! $SUDO $tool install $yes_flag $pkgs; then echo "Could not install OS dependencies. Aborting bootstrap!" exit 1 @@ -565,8 +590,9 @@ if [ "$1" = "--le-auto-phase2" ]; then # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" # This is the flattened list of packages certbot-auto installs. To generate -# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and -# then use `hashin` or a more secure method to gather the hashes. +# this, do +# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`, +# and then use `hashin` or a more secure method to gather the hashes. argparse==1.4.0 \ --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ @@ -598,28 +624,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.2.3 \ - --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ - --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ - --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ - --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ - --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ - --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ - --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ - --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ - --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ - --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ - --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ - --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ - --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ - --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ - --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ - --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ - --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ - --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ - --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ - --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ - --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +cryptography==1.3.4 \ + --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \ + --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \ + --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \ + --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \ + --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \ + --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \ + --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \ + --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \ + --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \ + --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \ + --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \ + --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \ + --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \ + --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \ + --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \ + --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \ + --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \ + --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \ + --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \ + --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \ + --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \ + --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 @@ -645,28 +672,6 @@ parsedatetime==2.1 \ pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 -psutil==3.3.0 \ - --hash=sha256:584f0b29fcc5d523b433cb8918b2fc74d67e30ee0b44a95baf031528f424619f \ - --hash=sha256:28ca0b6e9d99aa8dc286e8747a4471362b69812a25291de29b6a8d70a1545a0d \ - --hash=sha256:167ad5fff52a672c4ddc1c1a0b25146d6813ebb08a9aab0a3ac45f8a5b669c3b \ - --hash=sha256:e6dea6173a988727bb223d3497349ad5cdef5c0b282eff2d83e5f9065c53f85f \ - --hash=sha256:2af5e0a4aad66049955d0734aa4e3dc8caa17a9eaf8b4c1a27a5f1ee6e40f6fc \ - --hash=sha256:d9884dc0dc2e55e2448e495778dc9899c1c8bf37aeb2f434c1bea74af93c2683 \ - --hash=sha256:e27c2fe6dfcc8738be3d2c5a022f785eb72971057e1a9e1e34fba73bce8a71a6 \ - --hash=sha256:65afd6fecc8f3aed09ee4be63583bc8eb472f06ceaa4fe24c4d1d5a1a3c0e13f \ - --hash=sha256:ba1c558fbfcdf94515c2394b1155c1dc56e2bc2a9c17d30349827c9ed8a67e46 \ - --hash=sha256:ba95ea0022dcb64d36f0c1335c0605fae35bdf3e0fea8d92f5d0f6456a35e55b \ - --hash=sha256:421b6591d16b509aaa8d8c15821d66bb94cb4a8dc4385cad5c51b85d4a096d85 \ - --hash=sha256:326b305cbdb6f94dafbfe2c26b11da88b0ab07b8a07f8188ab9d75ff0c6e841a \ - --hash=sha256:9aede5b2b6fe46b3748ea8e5214443890d1634027bef3d33b7dad16556830278 \ - --hash=sha256:73bed1db894d1aa9c3c7e611d302cdeab7ae8a0dc0eeaf76727878db1ac5cd87 \ - --hash=sha256:935b5dd6d558af512f42501a7c08f41d7aff139af1bb3959daa3abb859234d6c \ - --hash=sha256:4ca0111cf157dcc0f2f69a323c5b5478718d68d45fc9435d84be0ec0f186215b \ - --hash=sha256:b6f13c95398a3fcf0226c4dcfa448560ba5865259cd96ec2810658651e932189 \ - --hash=sha256:ee6be30d1635bbdea4c4325d507dc8a0dbbde7e1c198bd62ddb9f43198b9e214 \ - --hash=sha256:dfa786858c268d7fbbe1b6175e001ec02738d7cfae0a7ce77bf9b651af676729 \ - --hash=sha256:aa77f9de72af9c16cc288cd4a24cf58824388f57d7a81e400c4616457629870e \ - --hash=sha256:f500093357d04da8140d87932cac2e54ef592a54ca8a743abb2850f60c2c22eb pyasn1==0.1.9 \ --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ @@ -679,9 +684,18 @@ pyasn1==0.1.9 \ --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f -pyOpenSSL==0.15.1 \ - --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ - --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyopenssl==16.0.0 \ + --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ + --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d +pyparsing==2.1.8 \ + --hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \ + --hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \ + --hash=sha256:ab09aee814c0241ff0c503cff30018219fe1fc14501d89f406f4664a0ec9fbcd \ + --hash=sha256:6e9a7f052f8e26bcf749e4033e3115b6dc7e3c85aafcb794b9a88c9d9ef13c97 \ + --hash=sha256:9f463a6bcc4eeb6c08f1ed84439b17818e2085937c0dee0d7674ac127c67c12b \ + --hash=sha256:3626b4d81cfb300dad57f52f2f791caaf7b06c09b368c0aa7b868e53a5775424 \ + --hash=sha256:367b90cc877b46af56d4580cd0ae278062903f02b8204ab631f5a2c0f50adfd0 \ + --hash=sha256:9f1ea360086cd68681e7f4ca8f1f38df47bf81942a0d76a9673c2d23eff35b13 pyRFC3339==1.0 \ --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 @@ -747,15 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.1 \ - --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ - --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f -certbot==0.8.1 \ - --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ - --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 -certbot-apache==0.8.1 \ - --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ - --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed +acme==0.9.0 \ + --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ + --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 +certbot==0.9.0 \ + --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ + --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e +certbot-apache==0.9.0 \ + --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ + --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a +certbot-nginx==0.9.0 \ + --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ + --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -912,13 +929,19 @@ UNLIKELY_EOF # Set PATH so pipstrap upgrades the right (v)env: PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e - PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + if [ "$VERBOSE" = 1 ]; then + "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + else + PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + fi PIP_STATUS=$? set -e if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) - echo "Had a problem while installing Python packages:" - echo "$PIP_OUT" + echo "Had a problem while installing Python packages." + if [ "$VERBOSE" != 1 ]; then + echo "$PIP_OUT" + fi rm -rf "$VENV_PATH" exit 1 fi @@ -926,8 +949,10 @@ UNLIKELY_EOF fi if [ -n "$SUDO" ]; then # SUDO is su wrapper or sudo - echo "Requesting root privileges to run certbot..." - echo " $VENV_BIN/letsencrypt" "$@" + if [ "$QUIET" != 1 ]; then + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop @@ -967,7 +992,7 @@ else # Print latest released version of LE to stdout: python fetch.py --latest-version - + # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm # in, and make sure its signature verifies: python fetch.py --le-auto-script v1.2.3 diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index fb56be65f..32e5935fb 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.10.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 62c705b4c..4c39d37c2 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.10.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 34358a5d9..45892e269 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.9.0.dev0' +__version__ = '0.10.0.dev0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 749983d0e..f7340c48b 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -21,7 +21,7 @@ optional arguments: config file path (default: None) -v, --verbose This flag can be used multiple times to incrementally increase the verbosity of output, e.g. -vvv. (default: - -3) + -2) -t, --text Use the text output instead of the curses UI. (default: False) -n, --non-interactive, --noninteractive @@ -29,7 +29,11 @@ optional arguments: require additional command line flags; the client will try to explain which ones are required if it finds one missing (default: False) - --dialog Run using dialog (default: False) + --dialog Run using interactive dialog menus (default: False) + -d DOMAIN, --domains DOMAIN, --domain DOMAIN + Domain names to apply. For multiple domains you can + use multiple -d flags or enter a comma separated list + of domains as a parameter. (default: []) --dry-run Perform a test run of the client, obtaining test (invalid) certs but not saving them to disk. This can currently only be used with the 'certonly' and 'renew' @@ -62,10 +66,16 @@ optional arguments: -m EMAIL, --email EMAIL Email used for registration and recovery contact. (default: None) - -d DOMAIN, --domains DOMAIN, --domain DOMAIN - Domain names to apply. For multiple domains you can - use multiple -d flags or enter a comma separated list - of domains as a parameter. (default: []) + --preferred-challenges PREF_CHALLS + A sorted, comma delimited list of the preferred + challenge to use during authorization with the most + preferred challenge listed first (Eg, "dns" or "tls- + sni-01,http,dns"). Not all plugins support all + challenges. See + https://certbot.eff.org/docs/using.html#plugins for + details. ACME Challenges are versioned, but if you + pick "http" rather than "http-01", Certbot will select + the latest version automatically. (default: []) --user-agent USER_AGENT Set a custom user agent string for the client. User agent strings allow the CA to collect high level @@ -104,35 +114,15 @@ automation: --duplicate Allow making a certificate lineage that duplicates an existing one (both can be renewed in parallel) (default: False) - --os-packages-only (letsencrypt-auto only) install OS package - dependencies and then stop (default: False) - --no-self-upgrade (letsencrypt-auto only) prevent the letsencrypt-auto - script from upgrading itself to newer released - versions (default: False) + --os-packages-only (certbot-auto only) install OS package dependencies + and then stop (default: False) + --no-self-upgrade (certbot-auto only) prevent the certbot-auto script + from upgrading itself to newer released versions + (default: False) -q, --quiet Silence all output except errors. Useful for automation via cron. Implies --non-interactive. (default: False) -testing: - The following flags are meant for testing purposes only! Do NOT change - them, unless you really know what you're doing! - - --debug Show tracebacks in case of errors, and allow - letsencrypt-auto execution on experimental platforms - (default: False) - --no-verify-ssl Disable SSL certificate verification. (default: False) - --tls-sni-01-port TLS_SNI_01_PORT - Port number to perform tls-sni-01 challenge. Boulder - in testing mode defaults to 5001. (default: 443) - --http-01-port HTTP01_PORT - Port used in the SimpleHttp challenge. (default: 80) - --break-my-certs Be willing to replace or renew valid certs with - invalid (testing/staging) certs (default: False) - --test-cert, --staging - Use the staging server to obtain test (invalid) certs; - equivalent to --server https://acme- - staging.api.letsencrypt.org/directory (default: False) - security: Security parameters & server settings @@ -147,8 +137,8 @@ security: HTTPS for the newly authenticated vhost. (default: None) --hsts Add the Strict-Transport-Security header to every HTTP - response. Forcing browser to always use SSL for - the domain. Defends against SSL Stripping. (default: + response. Forcing browser to always use SSL for the + domain. Defends against SSL Stripping. (default: False) --no-hsts Do not automatically add the Strict-Transport-Security header to every HTTP response. (default: False) @@ -168,6 +158,22 @@ security: current user; only needed if your config is somewhere unsafe like /tmp/ (default: False) +testing: + The following flags are meant for testing purposes only! Do NOT change + them, unless you really know what you're doing! + + --test-cert, --staging + Use the staging server to obtain test (invalid) certs; + equivalent to --server https://acme- + staging.api.letsencrypt.org/directory (default: False) + --debug Show tracebacks in case of errors, and allow certbot- + auto execution on experimental platforms (default: + False) + --no-verify-ssl Disable verification of the ACME server's certificate. + (default: False) + --break-my-certs Be willing to replace or renew valid certs with + invalid (testing/staging) certs (default: False) + renew: The 'renew' subcommand will attempt to renew all certificates (or more precisely, certificate lineages) you have previously obtained if they are @@ -194,11 +200,11 @@ renew: (default: None) --renew-hook RENEW_HOOK Command to be run in a shell once for each - successfully renewed certificate.For this command, the - shell variable $RENEWED_LINEAGE will point to - theconfig live subdirectory containing the new certs - and keys; the shell variable $RENEWED_DOMAINS will - contain a space-delimited list of renewed cert domains + successfully renewed certificate. For this command, + the shell variable $RENEWED_LINEAGE will point to the + config live subdirectory containing the new certs and + keys; the shell variable $RENEWED_DOMAINS will contain + a space-delimited list of renewed cert domains (default: None) --disable-hook-validation Ordinarily the commands specified for --pre-hook @@ -213,6 +219,16 @@ renew: certonly: Options for modifying how a cert is obtained + --tls-sni-01-port TLS_SNI_01_PORT + Port used during tls-sni-01 challenge. This only + affects the port Certbot listens on. A conforming ACME + server will still attempt to connect on port 443. + (default: 443) + --http-01-port HTTP01_PORT + Port used in the http-01 challenge.This only affects + the port Certbot listens on. A conforming ACME server + will still attempt to connect on port 80. (default: + 80) --csr CSR Path to a Certificate Signing Request (CSR) in DER format; note that the .csr file *must* contain a Subject Alternative Name field for each domain you @@ -232,7 +248,7 @@ rollback: (default: 1) plugins: - Plugin options + Options for the "plugins" subcommand --init Initialize plugins. (default: False) --prepare Initialize and prepare plugins. (default: False) @@ -267,10 +283,11 @@ paths: https://acme-v01.api.letsencrypt.org/directory) plugins: - Certbot client supports an extensible plugins architecture. See 'certbot - plugins' for a list of all installed plugins and their names. You can - force a particular plugin by setting options provided below. Running - --help will list flags specific to that plugin. + Plugin Selection: Certbot client supports an extensible plugins + architecture. See 'certbot plugins' for a list of all installed plugins + and their names. You can force a particular plugin by setting options + provided below. Running --help will list flags specific to + that plugin. -a AUTHENTICATOR, --authenticator AUTHENTICATOR Authenticator plugin name. (default: None) @@ -290,12 +307,17 @@ plugins: --webroot Obtain certs by placing files in a webroot directory. (default: False) -standalone: - Automatically use a temporary webserver +nginx: + Nginx Web Server plugin - Alpha - --standalone-supported-challenges STANDALONE_SUPPORTED_CHALLENGES - Supported challenges. Preferred in the order they are - listed. (default: tls-sni-01,http-01) + --nginx-server-root NGINX_SERVER_ROOT + Nginx server root directory. (default: /etc/nginx) + --nginx-ctl NGINX_CTL + Path to the 'nginx' binary, used for 'configtest' and + retrieving nginx version number. (default: nginx) + +standalone: + Spin up a temporary webserver manual: Manually configure an HTTP server @@ -306,15 +328,6 @@ manual: Automatically allows public IP logging. (default: False) -nginx: - Nginx Web Server - currently doesn't work - - --nginx-server-root NGINX_SERVER_ROOT - Nginx server root directory. (default: /etc/nginx) - --nginx-ctl NGINX_CTL - Path to the 'nginx' binary, used for 'configtest' and - retrieving nginx version number. (default: nginx) - webroot: Place files in webroot directory @@ -337,7 +350,7 @@ webroot: {}) apache: - Apache Web Server - Alpha + Apache Web Server plugin - Beta --apache-enmod APACHE_ENMOD Path to the Apache 'a2enmod' binary. (default: @@ -353,6 +366,9 @@ apache: --apache-vhost-root APACHE_VHOST_ROOT Apache server VirtualHost configuration root (default: /etc/apache2/sites-available) + --apache-logs-root APACHE_LOGS_ROOT + Apache server logs directory (default: + /var/log/apache2) --apache-challenge-location APACHE_CHALLENGE_LOCATION Directory path for challenge configuration. (default: /etc/apache2) diff --git a/letsencrypt-auto b/letsencrypt-auto index 80f39cf59..27fcde319 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.1" +LE_AUTO_VERSION="0.9.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -34,6 +34,7 @@ Help for certbot itself cannot be provided until it is installed. -n, --non-interactive, --noninteractive run without asking for user input --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + -q, --quiet provide only update/error output -v, --verbose provide more output All arguments are accepted and forwarded to the Certbot client when run." @@ -52,15 +53,19 @@ for arg in "$@" ; do HELP=1;; --noninteractive|--non-interactive) ASSUME_YES=1;; + --quiet) + QUIET=1;; --verbose) VERBOSE=1;; -[!-]*) - while getopts ":hnv" short_arg $arg; do + while getopts ":hnvq" short_arg $arg; do case "$short_arg" in h) HELP=1;; n) ASSUME_YES=1;; + q) + QUIET=1;; v) VERBOSE=1;; esac @@ -121,7 +126,7 @@ ExperimentalBootstrap() { $2 fi else - echo "WARNING: $1 support is very experimental at present..." + echo "FATAL: $1 support is very experimental at present..." echo "if you would like to work on improving it, please ensure you have backups" echo "and then run this script again with the --debug flag!" exit 1 @@ -276,6 +281,30 @@ BootstrapRpmCommon() { exit 1 fi + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then + echo "To use Certbot, packages from the EPEL repository need to be installed." + if ! $SUDO $tool list epel-release >/dev/null 2>&1; then + echo "Please enable this repository and try running Certbot again." + exit 1 + fi + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s + fi + if ! $SUDO $tool install $yes_flag epel-release; then + echo "Could not enable EPEL. Aborting bootstrap!" + exit 1 + fi + fi + pkgs=" gcc dialog @@ -313,10 +342,6 @@ BootstrapRpmCommon() { " fi - if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" - fi - if ! $SUDO $tool install $yes_flag $pkgs; then echo "Could not install OS dependencies. Aborting bootstrap!" exit 1 @@ -565,8 +590,9 @@ if [ "$1" = "--le-auto-phase2" ]; then # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" # This is the flattened list of packages certbot-auto installs. To generate -# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and -# then use `hashin` or a more secure method to gather the hashes. +# this, do +# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`, +# and then use `hashin` or a more secure method to gather the hashes. argparse==1.4.0 \ --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ @@ -598,28 +624,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.2.3 \ - --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ - --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ - --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ - --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ - --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ - --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ - --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ - --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ - --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ - --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ - --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ - --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ - --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ - --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ - --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ - --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ - --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ - --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ - --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ - --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ - --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +cryptography==1.3.4 \ + --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \ + --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \ + --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \ + --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \ + --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \ + --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \ + --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \ + --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \ + --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \ + --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \ + --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \ + --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \ + --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \ + --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \ + --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \ + --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \ + --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \ + --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \ + --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \ + --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \ + --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \ + --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 @@ -645,28 +672,6 @@ parsedatetime==2.1 \ pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 -psutil==3.3.0 \ - --hash=sha256:584f0b29fcc5d523b433cb8918b2fc74d67e30ee0b44a95baf031528f424619f \ - --hash=sha256:28ca0b6e9d99aa8dc286e8747a4471362b69812a25291de29b6a8d70a1545a0d \ - --hash=sha256:167ad5fff52a672c4ddc1c1a0b25146d6813ebb08a9aab0a3ac45f8a5b669c3b \ - --hash=sha256:e6dea6173a988727bb223d3497349ad5cdef5c0b282eff2d83e5f9065c53f85f \ - --hash=sha256:2af5e0a4aad66049955d0734aa4e3dc8caa17a9eaf8b4c1a27a5f1ee6e40f6fc \ - --hash=sha256:d9884dc0dc2e55e2448e495778dc9899c1c8bf37aeb2f434c1bea74af93c2683 \ - --hash=sha256:e27c2fe6dfcc8738be3d2c5a022f785eb72971057e1a9e1e34fba73bce8a71a6 \ - --hash=sha256:65afd6fecc8f3aed09ee4be63583bc8eb472f06ceaa4fe24c4d1d5a1a3c0e13f \ - --hash=sha256:ba1c558fbfcdf94515c2394b1155c1dc56e2bc2a9c17d30349827c9ed8a67e46 \ - --hash=sha256:ba95ea0022dcb64d36f0c1335c0605fae35bdf3e0fea8d92f5d0f6456a35e55b \ - --hash=sha256:421b6591d16b509aaa8d8c15821d66bb94cb4a8dc4385cad5c51b85d4a096d85 \ - --hash=sha256:326b305cbdb6f94dafbfe2c26b11da88b0ab07b8a07f8188ab9d75ff0c6e841a \ - --hash=sha256:9aede5b2b6fe46b3748ea8e5214443890d1634027bef3d33b7dad16556830278 \ - --hash=sha256:73bed1db894d1aa9c3c7e611d302cdeab7ae8a0dc0eeaf76727878db1ac5cd87 \ - --hash=sha256:935b5dd6d558af512f42501a7c08f41d7aff139af1bb3959daa3abb859234d6c \ - --hash=sha256:4ca0111cf157dcc0f2f69a323c5b5478718d68d45fc9435d84be0ec0f186215b \ - --hash=sha256:b6f13c95398a3fcf0226c4dcfa448560ba5865259cd96ec2810658651e932189 \ - --hash=sha256:ee6be30d1635bbdea4c4325d507dc8a0dbbde7e1c198bd62ddb9f43198b9e214 \ - --hash=sha256:dfa786858c268d7fbbe1b6175e001ec02738d7cfae0a7ce77bf9b651af676729 \ - --hash=sha256:aa77f9de72af9c16cc288cd4a24cf58824388f57d7a81e400c4616457629870e \ - --hash=sha256:f500093357d04da8140d87932cac2e54ef592a54ca8a743abb2850f60c2c22eb pyasn1==0.1.9 \ --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ @@ -679,9 +684,18 @@ pyasn1==0.1.9 \ --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f -pyOpenSSL==0.15.1 \ - --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ - --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyopenssl==16.0.0 \ + --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ + --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d +pyparsing==2.1.8 \ + --hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \ + --hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \ + --hash=sha256:ab09aee814c0241ff0c503cff30018219fe1fc14501d89f406f4664a0ec9fbcd \ + --hash=sha256:6e9a7f052f8e26bcf749e4033e3115b6dc7e3c85aafcb794b9a88c9d9ef13c97 \ + --hash=sha256:9f463a6bcc4eeb6c08f1ed84439b17818e2085937c0dee0d7674ac127c67c12b \ + --hash=sha256:3626b4d81cfb300dad57f52f2f791caaf7b06c09b368c0aa7b868e53a5775424 \ + --hash=sha256:367b90cc877b46af56d4580cd0ae278062903f02b8204ab631f5a2c0f50adfd0 \ + --hash=sha256:9f1ea360086cd68681e7f4ca8f1f38df47bf81942a0d76a9673c2d23eff35b13 pyRFC3339==1.0 \ --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 @@ -747,15 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.1 \ - --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ - --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f -certbot==0.8.1 \ - --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ - --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 -certbot-apache==0.8.1 \ - --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ - --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed +acme==0.9.0 \ + --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ + --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 +certbot==0.9.0 \ + --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ + --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e +certbot-apache==0.9.0 \ + --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ + --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a +certbot-nginx==0.9.0 \ + --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ + --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -912,13 +929,19 @@ UNLIKELY_EOF # Set PATH so pipstrap upgrades the right (v)env: PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e - PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + if [ "$VERBOSE" = 1 ]; then + "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + else + PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + fi PIP_STATUS=$? set -e if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) - echo "Had a problem while installing Python packages:" - echo "$PIP_OUT" + echo "Had a problem while installing Python packages." + if [ "$VERBOSE" != 1 ]; then + echo "$PIP_OUT" + fi rm -rf "$VENV_PATH" exit 1 fi @@ -926,8 +949,10 @@ UNLIKELY_EOF fi if [ -n "$SUDO" ]; then # SUDO is su wrapper or sudo - echo "Requesting root privileges to run certbot..." - echo " $VENV_BIN/letsencrypt" "$@" + if [ "$QUIET" != 1 ]; then + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop @@ -967,7 +992,7 @@ else # Print latest released version of LE to stdout: python fetch.py --latest-version - + # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm # in, and make sure its signature verifies: python fetch.py --le-auto-script v1.2.3 diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 5bb725c96..ed1adc5d0 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXYJmBAAoJEE0XyZXNl3XyyIMH/jtYFb7rl5XXN8hjlKuK5frq -z7/jdK7fvI+mtYJ4i2Cy3yMz8T4wscXGkhxNtipbATWlpevPfjYzm4ZGC25coFZx -fDX44w0hBBgel7EISXGR1ABXb2rj24TZxIYXwaeClylsK9n5CxcWBocn8tDlfr8t -7VQUJEL3l1IlrnKnvpoL4Eq11sxlIPtitDPJ5c98ZM1293ZbWzIqyZKoXLIUkKHg -pkaa80j/QMmFumxzXFenU91JusLdeoblvjjg+kzjGonjslAYIuH4wEEjz2VJuUYe -P2+2ZyW4eLA6rRZhZ3CMtV79HzTPTWiELCYbXezb+yXJJEqzCYtIXkmbNQ3jUEY= -=86lB +iQEcBAABAgAGBQJX9ENHAAoJEE0XyZXNl3XyFzIH/ibJU3UOKdHWeHEq+MBCqqaN +1oJJ7kGUpnvE+jqXpfuvKjchQjj8369ht9KW9CLXbyZycHBLYkaqYznZ69e/0Xg7 +BDs3XNGxOxVrOE4nZngqyUa3vwuT5fjq55fhUJtKaYHuKb2hkkcs0GneDt0UfTMR +orw0OSII5OouR6CyP6I3nnyyEkt/o4Hpt0nXyf7Q+vLh1+7ljjf3FhFd52D/kfxa +i1XoiHq5WdXnnqDDS3RQj+0KAPLm31rfM3kN201+HIxamtRJaPPX+v3iXkATtKGf +CiB/PD/ZSAS6priPThhwj3bRId2zE+PQDha0TE18qHyaGvCACG8uTAIWGoQimWY= +=KF0F -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index a7c98790d..ee9f11272 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.0.dev0" +LE_AUTO_VERSION="0.10.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,15 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.1 \ - --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ - --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f -certbot==0.8.1 \ - --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ - --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 -certbot-apache==0.8.1 \ - --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ - --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed +acme==0.9.0 \ + --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ + --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 +certbot==0.9.0 \ + --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ + --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e +certbot-apache==0.9.0 \ + --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ + --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a +certbot-nginx==0.9.0 \ + --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ + --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index e3da0e0a35a6dafe26dbbe061f546aeb929dfcdb..fd3a3ba8a33de8890dd2a652d10aeb2f1feeb87c 100644 GIT binary patch literal 256 zcmV+b0ssCaP0-VmT^Zaulp4$n|jriq$Ob}(z zpw0!bJ9aW}0B10DLIlQqBO4)Zzq~}P(8Gy> zOJ-LGt+9ha9gA_+N&zB_*dQCAcZuH1rAyFRt%Wy7h=IbIZCNaoc2Y#3;ujwk>17~k zNj{XBj~F04jH>;gmA%0h(}GM?JuM9I-+sE5&5@?b;V8wXnSYWY@reJu{#UVlBrThxmxIb7 G&VdBMz>LuV diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 282a8dfe5..4739f5ae1 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -170,12 +170,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.1 \ - --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ - --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f -certbot==0.8.1 \ - --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ - --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 -certbot-apache==0.8.1 \ - --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ - --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed +acme==0.9.0 \ + --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ + --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 +certbot==0.9.0 \ + --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ + --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e +certbot-apache==0.9.0 \ + --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ + --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a +certbot-nginx==0.9.0 \ + --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ + --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 From dcb3fb7382e6b1fdb8982ade399c5621d29ad32e Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 5 Oct 2016 12:28:38 -0700 Subject: [PATCH 192/331] Use correct Content-Types in headers. (#3566) * Add Content-Type: app/jose+json to post requests. * Add tests for proper content type. --- acme/acme/client.py | 4 +++- acme/acme/client_test.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index de7eef299..6a648bb92 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -495,6 +495,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes class ClientNetwork(object): # pylint: disable=too-many-instance-attributes """Client network.""" JSON_CONTENT_TYPE = 'application/json' + JOSE_CONTENT_TYPE = 'application/jose+json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json' REPLAY_NONCE_HEADER = 'Replay-Nonce' @@ -641,9 +642,10 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes self._add_nonce(self.head(url)) return self._nonces.pop() - def post(self, url, obj, content_type=JSON_CONTENT_TYPE, **kwargs): + def post(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs): """POST object wrapped in `.JWS` and check response.""" data = self._wrap_in_jws(obj, self._get_nonce(url)) + kwargs.setdefault('headers', {'Content-Type': content_type}) response = self._send_request('POST', url, data=data, **kwargs) self._add_nonce(response) return self._check_response(response, content_type=content_type) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 585576e2d..374f8954c 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -630,6 +630,10 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.send_request.assert_called_once_with( 'GET', 'http://example.com/', bar='baz') + def test_post_no_content_type(self): + self.content_type = self.net.JOSE_CONTENT_TYPE + self.assertEqual(self.checked_response, self.net.post('uri', self.obj)) + def test_post(self): # pylint: disable=protected-access self.assertEqual(self.checked_response, self.net.post( From 0b792e46b7a1984aaa71c1641c131a9bde967aec Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 5 Oct 2016 18:16:03 -0700 Subject: [PATCH 193/331] fix requirements.txt surgery in response to shipping certbot-nginx (#3585) --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index 7747b0e2b..f5c78da27 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -176,7 +176,7 @@ if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*12 " ; then fi # perform hideous surgery on requirements.txt... -head -n -9 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ +head -n -12 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ cat /tmp/hashes.$$ >> /tmp/req.$$ cp /tmp/req.$$ letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt From 0864f4e69248101f468e2d248c1688c0086c20d1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 6 Oct 2016 14:14:43 -0700 Subject: [PATCH 194/331] Make --quiet reduce the logging level (#3593) * reduce logging level and ignore verbose flags in quiet mode * Simplify setup_logging parameters The extra parameters were there in the past when the letsencrypt-renewer was a separate executable that also used this function. This is cruft that can be removed. * Add basic tests for setup_logging --- certbot/constants.py | 3 +++ certbot/main.py | 20 +++++++++++++------ certbot/tests/main_test.py | 40 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/certbot/constants.py b/certbot/constants.py index ae998e15a..117301380 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -37,6 +37,9 @@ STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory" """Defaults for CLI flags and `.IConfig` attributes.""" +QUIET_LOGGING_LEVEL = logging.WARNING +"""Logging level to use in quiet mode.""" + RENEWER_DEFAULTS = dict( renewer_enabled="yes", renew_before_expiry="30 days", diff --git a/certbot/main.py b/certbot/main.py index dd497d14d..35008fd62 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -625,14 +625,22 @@ def _cli_log_handler(config, level, fmt): return handler -def setup_logging(config, cli_handler_factory, logfile): - """Setup logging.""" - file_fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" +def setup_logging(config): + """Sets up logging to logfiles and the terminal. + + :param certbot.interface.IConfig config: Configuration object + + """ cli_fmt = "%(message)s" - level = -config.verbose_count * 10 + file_fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" + logfile = "letsencrypt.log" + if config.quiet: + level = constants.QUIET_LOGGING_LEVEL + else: + level = -config.verbose_count * 10 file_handler, log_file_path = setup_log_file_handler( config, logfile=logfile, fmt=file_fmt) - cli_handler = cli_handler_factory(config, level, cli_fmt) + cli_handler = _cli_log_handler(config, level, cli_fmt) # TODO: use fileConfig? @@ -738,7 +746,7 @@ def main(cli_args=sys.argv[1:]): os.geteuid(), config.strict_permissions) # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery - setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') + setup_logging(config) cli.possible_deprecation_warning(config) logger.debug("certbot version: %s", certbot.__version__) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index fab1065c5..f7a6c5896 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -7,8 +7,11 @@ import unittest import mock from certbot import cli +from certbot import colored_logging +from certbot import constants from certbot import configuration from certbot import errors +from certbot import log from certbot.plugins import disco as plugins_disco class MainTest(unittest.TestCase): @@ -80,6 +83,43 @@ class SetupLogFileHandlerTest(unittest.TestCase): self.config, "test.log", "%s") +class SetupLoggingTest(unittest.TestCase): + """Tests for certbot.main.setup_logging.""" + + def setUp(self): + self.config = mock.Mock( + logs_dir=tempfile.mkdtemp(), + noninteractive_mode=False, quiet=False, text_mode=False, + verbose_count=constants.CLI_DEFAULTS['verbose_count']) + + def tearDown(self): + shutil.rmtree(self.config.logs_dir) + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.main import setup_logging + return setup_logging(*args, **kwargs) + + @mock.patch('certbot.main.logging.getLogger') + def test_defaults(self, mock_get_logger): + self._call(self.config) + + cli_handler = mock_get_logger().addHandler.call_args_list[0][0][0] + self.assertEqual(cli_handler.level, -self.config.verbose_count * 10) + self.assertTrue( + isinstance(cli_handler, log.DialogHandler)) + + @mock.patch('certbot.main.logging.getLogger') + def test_quiet_mode(self, mock_get_logger): + self.config.quiet = self.config.noninteractive_mode = True + self._call(self.config) + + cli_handler = mock_get_logger().addHandler.call_args_list[0][0][0] + self.assertEqual(cli_handler.level, constants.QUIET_LOGGING_LEVEL) + self.assertTrue( + isinstance(cli_handler, colored_logging.StreamHandler)) + + class MakeOrVerifyCoreDirTest(unittest.TestCase): """Tests for certbot.main.make_or_verify_core_dir.""" From 6d6924dcd2434f71cd81f4344175c88794579471 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 5 Oct 2016 18:16:03 -0700 Subject: [PATCH 195/331] fix requirements.txt surgery in response to shipping certbot-nginx (#3585) --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index 7747b0e2b..f5c78da27 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -176,7 +176,7 @@ if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*12 " ; then fi # perform hideous surgery on requirements.txt... -head -n -9 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ +head -n -12 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ cat /tmp/hashes.$$ >> /tmp/req.$$ cp /tmp/req.$$ letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt From c6f7d740a07866c948fdfe91036305c7cac34de4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 6 Oct 2016 14:14:43 -0700 Subject: [PATCH 196/331] Make --quiet reduce the logging level (#3593) * reduce logging level and ignore verbose flags in quiet mode * Simplify setup_logging parameters The extra parameters were there in the past when the letsencrypt-renewer was a separate executable that also used this function. This is cruft that can be removed. * Add basic tests for setup_logging --- certbot/constants.py | 3 +++ certbot/main.py | 20 +++++++++++++------ certbot/tests/main_test.py | 40 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/certbot/constants.py b/certbot/constants.py index ae998e15a..117301380 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -37,6 +37,9 @@ STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory" """Defaults for CLI flags and `.IConfig` attributes.""" +QUIET_LOGGING_LEVEL = logging.WARNING +"""Logging level to use in quiet mode.""" + RENEWER_DEFAULTS = dict( renewer_enabled="yes", renew_before_expiry="30 days", diff --git a/certbot/main.py b/certbot/main.py index dd497d14d..35008fd62 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -625,14 +625,22 @@ def _cli_log_handler(config, level, fmt): return handler -def setup_logging(config, cli_handler_factory, logfile): - """Setup logging.""" - file_fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" +def setup_logging(config): + """Sets up logging to logfiles and the terminal. + + :param certbot.interface.IConfig config: Configuration object + + """ cli_fmt = "%(message)s" - level = -config.verbose_count * 10 + file_fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" + logfile = "letsencrypt.log" + if config.quiet: + level = constants.QUIET_LOGGING_LEVEL + else: + level = -config.verbose_count * 10 file_handler, log_file_path = setup_log_file_handler( config, logfile=logfile, fmt=file_fmt) - cli_handler = cli_handler_factory(config, level, cli_fmt) + cli_handler = _cli_log_handler(config, level, cli_fmt) # TODO: use fileConfig? @@ -738,7 +746,7 @@ def main(cli_args=sys.argv[1:]): os.geteuid(), config.strict_permissions) # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery - setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') + setup_logging(config) cli.possible_deprecation_warning(config) logger.debug("certbot version: %s", certbot.__version__) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index fab1065c5..f7a6c5896 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -7,8 +7,11 @@ import unittest import mock from certbot import cli +from certbot import colored_logging +from certbot import constants from certbot import configuration from certbot import errors +from certbot import log from certbot.plugins import disco as plugins_disco class MainTest(unittest.TestCase): @@ -80,6 +83,43 @@ class SetupLogFileHandlerTest(unittest.TestCase): self.config, "test.log", "%s") +class SetupLoggingTest(unittest.TestCase): + """Tests for certbot.main.setup_logging.""" + + def setUp(self): + self.config = mock.Mock( + logs_dir=tempfile.mkdtemp(), + noninteractive_mode=False, quiet=False, text_mode=False, + verbose_count=constants.CLI_DEFAULTS['verbose_count']) + + def tearDown(self): + shutil.rmtree(self.config.logs_dir) + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.main import setup_logging + return setup_logging(*args, **kwargs) + + @mock.patch('certbot.main.logging.getLogger') + def test_defaults(self, mock_get_logger): + self._call(self.config) + + cli_handler = mock_get_logger().addHandler.call_args_list[0][0][0] + self.assertEqual(cli_handler.level, -self.config.verbose_count * 10) + self.assertTrue( + isinstance(cli_handler, log.DialogHandler)) + + @mock.patch('certbot.main.logging.getLogger') + def test_quiet_mode(self, mock_get_logger): + self.config.quiet = self.config.noninteractive_mode = True + self._call(self.config) + + cli_handler = mock_get_logger().addHandler.call_args_list[0][0][0] + self.assertEqual(cli_handler.level, constants.QUIET_LOGGING_LEVEL) + self.assertTrue( + isinstance(cli_handler, colored_logging.StreamHandler)) + + class MakeOrVerifyCoreDirTest(unittest.TestCase): """Tests for certbot.main.make_or_verify_core_dir.""" From eeac01c776d78b612d8b639b3dffc451964ddc82 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 6 Oct 2016 14:56:27 -0700 Subject: [PATCH 197/331] Release 0.9.1 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 26 +++++++++--------- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 18 ++++++------ letsencrypt-auto | 26 +++++++++--------- letsencrypt-auto-source/certbot-auto.asc | 14 +++++----- letsencrypt-auto-source/letsencrypt-auto | 26 +++++++++--------- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 24 ++++++++-------- 12 files changed, 72 insertions(+), 72 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 2b32f7e28..4bdfdefa0 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.10.0.dev0' +version = '0.9.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 2b4ac8563..fdfdae395 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.10.0.dev0' +version = '0.9.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 27fcde319..eb627032f 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.0" +LE_AUTO_VERSION="0.9.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.0 \ - --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ - --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 -certbot==0.9.0 \ - --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ - --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e -certbot-apache==0.9.0 \ - --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ - --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a -certbot-nginx==0.9.0 \ - --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ - --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 +acme==0.9.1 \ + --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ + --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 +certbot==0.9.1 \ + --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ + --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 +certbot-apache==0.9.1 \ + --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ + --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c +certbot-nginx==0.9.1 \ + --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ + --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 32e5935fb..9b4a7854d 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.10.0.dev0' +version = '0.9.1' install_requires = [ 'certbot', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 4c39d37c2..c434caef2 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.10.0.dev0' +version = '0.9.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 45892e269..0b082669f 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.10.0.dev0' +__version__ = '0.9.1' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index f7340c48b..7e321f407 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -307,15 +307,6 @@ plugins: --webroot Obtain certs by placing files in a webroot directory. (default: False) -nginx: - Nginx Web Server plugin - Alpha - - --nginx-server-root NGINX_SERVER_ROOT - Nginx server root directory. (default: /etc/nginx) - --nginx-ctl NGINX_CTL - Path to the 'nginx' binary, used for 'configtest' and - retrieving nginx version number. (default: nginx) - standalone: Spin up a temporary webserver @@ -328,6 +319,15 @@ manual: Automatically allows public IP logging. (default: False) +nginx: + Nginx Web Server plugin - Alpha + + --nginx-server-root NGINX_SERVER_ROOT + Nginx server root directory. (default: /etc/nginx) + --nginx-ctl NGINX_CTL + Path to the 'nginx' binary, used for 'configtest' and + retrieving nginx version number. (default: nginx) + webroot: Place files in webroot directory diff --git a/letsencrypt-auto b/letsencrypt-auto index 27fcde319..eb627032f 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.0" +LE_AUTO_VERSION="0.9.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.0 \ - --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ - --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 -certbot==0.9.0 \ - --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ - --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e -certbot-apache==0.9.0 \ - --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ - --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a -certbot-nginx==0.9.0 \ - --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ - --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 +acme==0.9.1 \ + --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ + --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 +certbot==0.9.1 \ + --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ + --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 +certbot-apache==0.9.1 \ + --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ + --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c +certbot-nginx==0.9.1 \ + --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ + --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index ed1adc5d0..71a54bde5 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJX9ENHAAoJEE0XyZXNl3XyFzIH/ibJU3UOKdHWeHEq+MBCqqaN -1oJJ7kGUpnvE+jqXpfuvKjchQjj8369ht9KW9CLXbyZycHBLYkaqYznZ69e/0Xg7 -BDs3XNGxOxVrOE4nZngqyUa3vwuT5fjq55fhUJtKaYHuKb2hkkcs0GneDt0UfTMR -orw0OSII5OouR6CyP6I3nnyyEkt/o4Hpt0nXyf7Q+vLh1+7ljjf3FhFd52D/kfxa -i1XoiHq5WdXnnqDDS3RQj+0KAPLm31rfM3kN201+HIxamtRJaPPX+v3iXkATtKGf -CiB/PD/ZSAS6priPThhwj3bRId2zE+PQDha0TE18qHyaGvCACG8uTAIWGoQimWY= -=KF0F +iQEcBAABAgAGBQJX9shpAAoJEE0XyZXNl3XyeAUH/0928PysUZlLCQRpw3GPJnr0 +WgE1duULRfDKOdyoj8cIABEcxyK+rASyBju57Hx80Zuai9x4XSHJK7k9BXrZrU5k +KHZWbaNOKLN+C7/HTSOqGwalGTLglRJLZMwcj4rs8jtftg6GiWXvtnWuwqoiZJe4 +sCdddm2gu4D2VLp/QpBU6Gepuls4PmtB7SzwRUC6SAkWf5ntwJ4mq65bwKLcTPaZ +oRKoswo+eyiosH2SVVgiyAz7U96t2gxfK2pNoTdCUQGjuRaD6P2yBCdJA5h4L2l2 +W+/31zJpw/TgFErWpNuNYYoMh8cswaWXDNMUscsuduQ9KPLhSHQQ9JZ5f4w+9PM= +=Zhq9 -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index ee9f11272..eb627032f 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.10.0.dev0" +LE_AUTO_VERSION="0.9.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.0 \ - --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ - --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 -certbot==0.9.0 \ - --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ - --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e -certbot-apache==0.9.0 \ - --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ - --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a -certbot-nginx==0.9.0 \ - --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ - --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 +acme==0.9.1 \ + --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ + --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 +certbot==0.9.1 \ + --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ + --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 +certbot-apache==0.9.1 \ + --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ + --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c +certbot-nginx==0.9.1 \ + --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ + --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index fd3a3ba8a33de8890dd2a652d10aeb2f1feeb87c..80ec36e19ad2a8967d36cd409347726b08cdf0d7 100644 GIT binary patch literal 256 zcmV+b0ssCA_-8=flSy<6Fl!QgcZZkvQ*|qkSu6D;yM{EED_69C0x#H)Ik$m9EG-?> zJ3eBlvyacp^nP0-VmT^Zaulp4$n|jriq$Ob}(z zpw0!bJ9aW}0B10DLIlQqBO4)Zzq~}P(8Gy> zOJ-LGt+9ha9gA_+N&zB_*dQCAcZuH1rAyFRt%Wy7h=IbIZCNaoc2Y#3;ujwk>17~k zNj{XBj~F04jH>;gmA%0h(}GM?Ju Date: Thu, 6 Oct 2016 16:58:50 -0700 Subject: [PATCH 198/331] Release 0.9.1 (#3595) * fix requirements.txt surgery in response to shipping certbot-nginx (#3585) * Make --quiet reduce the logging level (#3593) * reduce logging level and ignore verbose flags in quiet mode * Simplify setup_logging parameters The extra parameters were there in the past when the letsencrypt-renewer was a separate executable that also used this function. This is cruft that can be removed. * Add basic tests for setup_logging * Release 0.9.1 * Bump version to 0.10.0 --- certbot-auto | 26 +++++++++--------- docs/cli-help.txt | 18 ++++++------ letsencrypt-auto | 26 +++++++++--------- letsencrypt-auto-source/certbot-auto.asc | 14 +++++----- letsencrypt-auto-source/letsencrypt-auto | 24 ++++++++-------- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 24 ++++++++-------- 7 files changed, 66 insertions(+), 66 deletions(-) diff --git a/certbot-auto b/certbot-auto index 27fcde319..eb627032f 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.0" +LE_AUTO_VERSION="0.9.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.0 \ - --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ - --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 -certbot==0.9.0 \ - --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ - --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e -certbot-apache==0.9.0 \ - --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ - --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a -certbot-nginx==0.9.0 \ - --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ - --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 +acme==0.9.1 \ + --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ + --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 +certbot==0.9.1 \ + --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ + --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 +certbot-apache==0.9.1 \ + --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ + --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c +certbot-nginx==0.9.1 \ + --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ + --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/docs/cli-help.txt b/docs/cli-help.txt index f7340c48b..7e321f407 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -307,15 +307,6 @@ plugins: --webroot Obtain certs by placing files in a webroot directory. (default: False) -nginx: - Nginx Web Server plugin - Alpha - - --nginx-server-root NGINX_SERVER_ROOT - Nginx server root directory. (default: /etc/nginx) - --nginx-ctl NGINX_CTL - Path to the 'nginx' binary, used for 'configtest' and - retrieving nginx version number. (default: nginx) - standalone: Spin up a temporary webserver @@ -328,6 +319,15 @@ manual: Automatically allows public IP logging. (default: False) +nginx: + Nginx Web Server plugin - Alpha + + --nginx-server-root NGINX_SERVER_ROOT + Nginx server root directory. (default: /etc/nginx) + --nginx-ctl NGINX_CTL + Path to the 'nginx' binary, used for 'configtest' and + retrieving nginx version number. (default: nginx) + webroot: Place files in webroot directory diff --git a/letsencrypt-auto b/letsencrypt-auto index 27fcde319..eb627032f 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.0" +LE_AUTO_VERSION="0.9.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.0 \ - --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ - --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 -certbot==0.9.0 \ - --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ - --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e -certbot-apache==0.9.0 \ - --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ - --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a -certbot-nginx==0.9.0 \ - --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ - --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 +acme==0.9.1 \ + --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ + --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 +certbot==0.9.1 \ + --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ + --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 +certbot-apache==0.9.1 \ + --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ + --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c +certbot-nginx==0.9.1 \ + --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ + --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index ed1adc5d0..71a54bde5 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJX9ENHAAoJEE0XyZXNl3XyFzIH/ibJU3UOKdHWeHEq+MBCqqaN -1oJJ7kGUpnvE+jqXpfuvKjchQjj8369ht9KW9CLXbyZycHBLYkaqYznZ69e/0Xg7 -BDs3XNGxOxVrOE4nZngqyUa3vwuT5fjq55fhUJtKaYHuKb2hkkcs0GneDt0UfTMR -orw0OSII5OouR6CyP6I3nnyyEkt/o4Hpt0nXyf7Q+vLh1+7ljjf3FhFd52D/kfxa -i1XoiHq5WdXnnqDDS3RQj+0KAPLm31rfM3kN201+HIxamtRJaPPX+v3iXkATtKGf -CiB/PD/ZSAS6priPThhwj3bRId2zE+PQDha0TE18qHyaGvCACG8uTAIWGoQimWY= -=KF0F +iQEcBAABAgAGBQJX9shpAAoJEE0XyZXNl3XyeAUH/0928PysUZlLCQRpw3GPJnr0 +WgE1duULRfDKOdyoj8cIABEcxyK+rASyBju57Hx80Zuai9x4XSHJK7k9BXrZrU5k +KHZWbaNOKLN+C7/HTSOqGwalGTLglRJLZMwcj4rs8jtftg6GiWXvtnWuwqoiZJe4 +sCdddm2gu4D2VLp/QpBU6Gepuls4PmtB7SzwRUC6SAkWf5ntwJ4mq65bwKLcTPaZ +oRKoswo+eyiosH2SVVgiyAz7U96t2gxfK2pNoTdCUQGjuRaD6P2yBCdJA5h4L2l2 +W+/31zJpw/TgFErWpNuNYYoMh8cswaWXDNMUscsuduQ9KPLhSHQQ9JZ5f4w+9PM= +=Zhq9 -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index ee9f11272..59aacba57 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.0 \ - --hash=sha256:6d98bdec5fd9171e368a39cb2458d2d62599e52e2899650684379dba03f09d96 \ - --hash=sha256:fca3e57e5155ee0e80852e1b13a9d76a302728b78a9a1c2c38cb77d699aab7c5 -certbot==0.9.0 \ - --hash=sha256:08652503bb94f0863c2c2d53d37fc48966b080a70f578f980b05eee11bea7eaf \ - --hash=sha256:8dd4d3e2de2b2392fd8f41e73434dae737653bc41a54ce4a273768519a5ab61e -certbot-apache==0.9.0 \ - --hash=sha256:10e3096ff27adf6e75aff7722ada963f1337ef455f203495c025e883d8f816e2 \ - --hash=sha256:fd8bb3e7d36c64a5841083bed447250e110d458323b51d8a210b8f0b18e28c9a -certbot-nginx==0.9.0 \ - --hash=sha256:edbb7c1164225af770af6aea42496b575b5c115afa4d9ab7b1e587b3e6d0c92b \ - --hash=sha256:2f18504832b13f11e07dc87df758a7c5c68e24341628eb334ff7a5976a384717 +acme==0.9.1 \ + --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ + --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 +certbot==0.9.1 \ + --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ + --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 +certbot-apache==0.9.1 \ + --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ + --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c +certbot-nginx==0.9.1 \ + --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ + --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index fd3a3ba8a33de8890dd2a652d10aeb2f1feeb87c..80ec36e19ad2a8967d36cd409347726b08cdf0d7 100644 GIT binary patch literal 256 zcmV+b0ssCA_-8=flSy<6Fl!QgcZZkvQ*|qkSu6D;yM{EED_69C0x#H)Ik$m9EG-?> zJ3eBlvyacp^nP0-VmT^Zaulp4$n|jriq$Ob}(z zpw0!bJ9aW}0B10DLIlQqBO4)Zzq~}P(8Gy> zOJ-LGt+9ha9gA_+N&zB_*dQCAcZuH1rAyFRt%Wy7h=IbIZCNaoc2Y#3;ujwk>17~k zNj{XBj~F04jH>;gmA%0h(}GM?Ju Date: Fri, 7 Oct 2016 00:18:05 -0700 Subject: [PATCH 199/331] Document the Nginx plugin release (#3588) * Document the Nginx plugin release * Tweak * Remove mrueg nginx instructions for now? * Shipped -> included * keep order of plugin descriptions consistent with the table --- docs/using.rst | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 18dca071a..4604fd78f 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -179,21 +179,6 @@ want to use the Apache plugin, it has to be installed separately: emerge -av app-crypt/letsencrypt emerge -av app-crypt/letsencrypt-apache -Currently, only the Apache plugin is included in Portage. However, if you -Warning! -You can use Layman to add the mrueg overlay which does include a package for the -Certbot Nginx plugin, however, this plugin is known to be buggy and should only -be used with caution after creating a backup up your Nginx configuration. -We strongly recommend you use the app-crypt/letsencrypt package instead until -the Nginx plugin is ready. - -.. code-block:: shell - - emerge -av app-portage/layman - layman -S - layman -a mrueg - emerge -av app-crypt/letsencrypt-nginx - When using the Apache plugin, you will run into a "cannot find a cert or key directive" error if you're sporting the default Gentoo ``httpd.conf``. You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf`` @@ -272,13 +257,14 @@ apache_ Y Y | Automates obtaining and installing a cert with Apache 2. | Debian-based distributions with ``libaugeas0`` 1.0+. webroot_ Y N | Obtains a cert by writing to the webroot directory of an http-01_ (80) | already running webserver. +nginx_ Y Y | Automates obtaining and installing a cert with Nginx. Alpha tls-sni-01_ (443) + | release shipped with Certbot 0.9.0. standalone_ Y N | Uses a "standalone" webserver to obtain a cert. Requires http-01_ (80) or | port 80 or 443 to be available. This is useful on systems tls-sni-01_ (443) | with no webserver, or when direct integration with the local | webserver is not supported or not desired. manual_ Y N | Helps you obtain a cert by giving you instructions to perform http-01_ (80) or | domain validation yourself. dns-01_ (53) -nginx_ Y Y | Very experimental and not included in certbot-auto_. tls-sni-01_ (443) =========== ==== ==== =============================================================== ============================= Under the hood, plugins use one of several ACME protocol "Challenges_" to @@ -349,6 +335,19 @@ your webserver configuration, you might need to modify the configuration to ensure that files inside ``/.well-known/acme-challenge`` are served by the webserver. +Nginx +----- + +The Nginx plugin has been distributed with Cerbot since version 0.9.0 and should +work for most configurations. Because it is alpha code, we recommend backing up Nginx +configurations before using it (though you can also revert changes to +configurations with ``certbot --nginx rollback``). You can use it by providing +the ``--nginx`` flag on the commandline. + +:: + + certbot --nginx + Standalone ---------- @@ -378,15 +377,6 @@ the UI, you can use the plugin to obtain a cert by specifying to copy and paste commands into another terminal session, which may be on a different computer. -Nginx ------ - -In the future, if you're running Nginx you will hopefully be able to use this -plugin to automatically obtain and install your certificate. The Nginx plugin is -still experimental, however, and is not installed with certbot-auto_. If -installed, you can select this plugin on the command line by including -``--nginx``. - .. _third-party-plugins: Third-party plugins From cb613ba7d3c0ea625882fb21ac21474d95e9146e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 10 Oct 2016 13:17:49 -0700 Subject: [PATCH 200/331] Create symlinks at runtime and don't use relative paths (#3600) * Create symlinks at runtime in cli_test.py * use test_util.vector_path rather than hardcoding path * Reference #2716 in comment about too many lines in cli.py --- certbot/tests/cli_test.py | 65 ++++++++++++++----- .../testdata/live/sample-renewal/cert.pem | 1 - .../testdata/live/sample-renewal/chain.pem | 1 - .../live/sample-renewal/fullchain.pem | 1 - .../testdata/live/sample-renewal/privkey.pem | 1 - .../cert1.pem | 0 .../chain1.pem | 0 .../fullchain1.pem | 0 .../privkey1.pem | 0 .../testdata/sample-renewal-ancient.conf | 8 +-- 10 files changed, 52 insertions(+), 25 deletions(-) delete mode 120000 certbot/tests/testdata/live/sample-renewal/cert.pem delete mode 120000 certbot/tests/testdata/live/sample-renewal/chain.pem delete mode 120000 certbot/tests/testdata/live/sample-renewal/fullchain.pem delete mode 120000 certbot/tests/testdata/live/sample-renewal/privkey.pem rename certbot/tests/testdata/{archive/sample-renewal => sample-archive}/cert1.pem (100%) rename certbot/tests/testdata/{archive/sample-renewal => sample-archive}/chain1.pem (100%) rename certbot/tests/testdata/{archive/sample-renewal => sample-archive}/fullchain1.pem (100%) rename certbot/tests/testdata/{archive/sample-renewal => sample-archive}/privkey1.pem (100%) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 39a54258a..fde634c4c 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -1,4 +1,7 @@ """Tests for certbot.cli.""" +# Many tests in this file should be moved into +# main_test.py and renewal_test.py. See #2716. +# pylint: disable=too-many-lines from __future__ import print_function import argparse @@ -575,7 +578,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, args=None, should_renew=True, error_expected=False): # pylint: disable=too-many-locals,too-many-arguments - cert_path = 'certbot/tests/testdata/cert.pem' + cert_path = test_util.vector_path('cert.pem') chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path) mock_lineage.should_autorenew.return_value = due_for_renewal @@ -651,32 +654,60 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], log_out="not yet due", should_renew=False) - def _dump_log(self): with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: print("Logs:") print(lf.read()) + def _make_lineage(self, testfile): + """Creates a lineage defined by testfile. - def _make_test_renewal_conf(self, testfile): + This creates the archive, live, and renewal directories if + necessary and creates a simple lineage. + + :param str testfile: configuration file to base the lineage on + + :returns: path to the renewal conf file for the created lineage + :rtype: str + + """ + lineage_name = testfile[:-len('.conf')] + + conf_dir = os.path.join( + self.config_dir, constants.RENEWAL_CONFIGS_DIR) + archive_dir = os.path.join( + self.config_dir, constants.ARCHIVE_DIR, lineage_name) + live_dir = os.path.join( + self.config_dir, constants.LIVE_DIR, lineage_name) + + for directory in (archive_dir, conf_dir, live_dir,): + if not os.path.exists(directory): + os.makedirs(directory) + + sample_archive = test_util.vector_path('sample-archive') + for kind in os.listdir(sample_archive): + shutil.copyfile(os.path.join(sample_archive, kind), + os.path.join(archive_dir, kind)) + + for kind in storage.ALL_FOUR: + os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)), + os.path.join(live_dir, '{0}.pem'.format(kind))) + + conf_path = os.path.join(self.config_dir, conf_dir, testfile) with open(test_util.vector_path(testfile)) as src: - # put the correct path for cert.pem, chain.pem etc in the renewal conf - renewal_conf = src.read().replace("MAGICDIR", test_util.vector_path()) - rd = os.path.join(self.config_dir, "renewal") - if not os.path.exists(rd): - os.makedirs(rd) - rc = os.path.join(rd, "sample-renewal.conf") - with open(rc, "w") as dest: - dest.write(renewal_conf) - return rc + with open(conf_path, 'w') as dst: + dst.writelines( + line.replace('MAGICDIR', self.config_dir) for line in src) + + return conf_path def test_renew_verb(self): - self._make_test_renewal_conf('sample-renewal.conf') + self._make_lineage('sample-renewal.conf') args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, should_renew=True) def test_quiet_renew(self): - self._make_test_renewal_conf('sample-renewal.conf') + self._make_lineage('sample-renewal.conf') args = ["renew", "--dry-run"] _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) out = stdout.getvalue() @@ -688,13 +719,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual("", out) def test_renew_hook_validation(self): - self._make_test_renewal_conf('sample-renewal.conf') + self._make_lineage('sample-renewal.conf') args = ["renew", "--dry-run", "--post-hook=no-such-command"] self._test_renewal_common(True, [], args=args, should_renew=False, error_expected=True) def test_renew_no_hook_validation(self): - self._make_test_renewal_conf('sample-renewal.conf') + self._make_lineage('sample-renewal.conf') args = ["renew", "--dry-run", "--post-hook=no-such-command", "--disable-hook-validation"] self._test_renewal_common(True, [], args=args, should_renew=True, @@ -703,7 +734,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods @mock.patch("certbot.cli.set_by_cli") def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): mock_set_by_cli.return_value = False - rc_path = self._make_test_renewal_conf('sample-renewal-ancient.conf') + rc_path = self._make_lineage('sample-renewal-ancient.conf') args = mock.MagicMock(account=None, email=None, webroot_path=None) config = configuration.NamespaceConfig(args) lineage = storage.RenewableCert(rc_path, diff --git a/certbot/tests/testdata/live/sample-renewal/cert.pem b/certbot/tests/testdata/live/sample-renewal/cert.pem deleted file mode 120000 index e06effe40..000000000 --- a/certbot/tests/testdata/live/sample-renewal/cert.pem +++ /dev/null @@ -1 +0,0 @@ -../../archive/sample-renewal/cert1.pem \ No newline at end of file diff --git a/certbot/tests/testdata/live/sample-renewal/chain.pem b/certbot/tests/testdata/live/sample-renewal/chain.pem deleted file mode 120000 index 71f665f29..000000000 --- a/certbot/tests/testdata/live/sample-renewal/chain.pem +++ /dev/null @@ -1 +0,0 @@ -../../archive/sample-renewal/chain1.pem \ No newline at end of file diff --git a/certbot/tests/testdata/live/sample-renewal/fullchain.pem b/certbot/tests/testdata/live/sample-renewal/fullchain.pem deleted file mode 120000 index 0f06f077d..000000000 --- a/certbot/tests/testdata/live/sample-renewal/fullchain.pem +++ /dev/null @@ -1 +0,0 @@ -../../archive/sample-renewal/fullchain1.pem \ No newline at end of file diff --git a/certbot/tests/testdata/live/sample-renewal/privkey.pem b/certbot/tests/testdata/live/sample-renewal/privkey.pem deleted file mode 120000 index 5187eda6b..000000000 --- a/certbot/tests/testdata/live/sample-renewal/privkey.pem +++ /dev/null @@ -1 +0,0 @@ -../../archive/sample-renewal/privkey1.pem \ No newline at end of file diff --git a/certbot/tests/testdata/archive/sample-renewal/cert1.pem b/certbot/tests/testdata/sample-archive/cert1.pem similarity index 100% rename from certbot/tests/testdata/archive/sample-renewal/cert1.pem rename to certbot/tests/testdata/sample-archive/cert1.pem diff --git a/certbot/tests/testdata/archive/sample-renewal/chain1.pem b/certbot/tests/testdata/sample-archive/chain1.pem similarity index 100% rename from certbot/tests/testdata/archive/sample-renewal/chain1.pem rename to certbot/tests/testdata/sample-archive/chain1.pem diff --git a/certbot/tests/testdata/archive/sample-renewal/fullchain1.pem b/certbot/tests/testdata/sample-archive/fullchain1.pem similarity index 100% rename from certbot/tests/testdata/archive/sample-renewal/fullchain1.pem rename to certbot/tests/testdata/sample-archive/fullchain1.pem diff --git a/certbot/tests/testdata/archive/sample-renewal/privkey1.pem b/certbot/tests/testdata/sample-archive/privkey1.pem similarity index 100% rename from certbot/tests/testdata/archive/sample-renewal/privkey1.pem rename to certbot/tests/testdata/sample-archive/privkey1.pem diff --git a/certbot/tests/testdata/sample-renewal-ancient.conf b/certbot/tests/testdata/sample-renewal-ancient.conf index dd3075b8e..333bcaa18 100644 --- a/certbot/tests/testdata/sample-renewal-ancient.conf +++ b/certbot/tests/testdata/sample-renewal-ancient.conf @@ -1,7 +1,7 @@ -cert = MAGICDIR/live/sample-renewal/cert.pem -privkey = MAGICDIR/live/sample-renewal/privkey.pem -chain = MAGICDIR/live/sample-renewal/chain.pem -fullchain = MAGICDIR/live/sample-renewal/fullchain.pem +cert = MAGICDIR/live/sample-renewal-ancient/cert.pem +privkey = MAGICDIR/live/sample-renewal-ancient/privkey.pem +chain = MAGICDIR/live/sample-renewal-ancient/chain.pem +fullchain = MAGICDIR/live/sample-renewal-ancient/fullchain.pem renew_before_expiry = 1 year # Options and defaults used in the renewal process From 2415092a78843c457149e91129362e3f4be932d1 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 10 Oct 2016 18:36:58 -0700 Subject: [PATCH 201/331] Update Where Are My Certs section. (#3419) * Update Where Are My Certs section. This combines the `cert.pem` and `chain.pem` sections into a single paragraph, making it clearer that they are closely connected. It also adds text indicating that they are less common and moves them below the section for `fullchain.pem`. * Update "Getting Help" section. * Add link to document missing intermediate. * Remove incorrect line about ordering. Also remove "(as the filename suggests)," and clarify file ordering in the fullchain.pem section. --- docs/using.rst | 63 +++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 4604fd78f..d18d118cf 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -587,43 +587,41 @@ The following files are available: This is what Apache needs for `SSLCertificateKeyFile `_, - and nginx for `ssl_certificate_key + and Nginx for `ssl_certificate_key `_. -``cert.pem`` - Server certificate only. - - This is what Apache < 2.4.8 needs for `SSLCertificateFile - `_. - -``chain.pem`` - All certificates that need to be served by the browser **excluding** - server certificate, i.e. root and intermediate certificates only. - - This is what Apache < 2.4.8 needs for `SSLCertificateChainFile - `_, - and what nginx >= 1.3.7 needs for `ssl_trusted_certificate - `_. - ``fullchain.pem`` - All certificates, **including** server certificate. This is - concatenation of ``cert.pem`` and ``chain.pem``. + All certificates, **including** server certificate (aka leaf certificate or + end-entity certificate). The server certificate is the first one in this file, + followed by any intermediates. This is what Apache >= 2.4.8 needs for `SSLCertificateFile `_, - and what nginx needs for `ssl_certificate + and what Nginx needs for `ssl_certificate `_. +``cert.pem`` and ``chain.pem`` (less common) + ``cert.pem`` contains the server certificate by itself, and + ``chain.pem`` contains the additional intermediate certificate or + certificates that web browsers will need in order to validate the + server certificate. If you provide one of these files to your web + server, you **must** provide both of them, or some browsers will show + "This Connection is Untrusted" errors for your site, `some of the time + `_. -For both chain files, all certificates are ordered from root (primary -certificate) towards leaf. + Apache < 2.4.8 needs these for `SSLCertificateFile + `_. + and `SSLCertificateChainFile + `_, + respectively. -Please note, that **you must use** either ``chain.pem`` or -``fullchain.pem``. In case of webservers, using only ``cert.pem``, -will cause nasty errors served through the browsers! + If you're using OCSP stapling with Nginx >= 1.3.7, ``chain.pem`` should be + provided as the `ssl_trusted_certificate + `_ + to validate OCSP responses. -.. note:: All files are PEM-encoded (as the filename suffix - suggests). If you need other format, such as DER or PFX, then you +.. note:: All files are PEM-encoded. + If you need other format, such as DER or PFX, then you could convert using ``openssl``. You can automate that with ``--renew-hook`` if you're using automatic renewal_. @@ -653,14 +651,15 @@ By default, the following locations are searched: Getting help ============ -If you're having problems you can chat with us on `IRC (#certbot @ -OFTC) `_ or at -`IRC (#letsencrypt @ freenode) `_ -or get support on the Let's Encrypt `forums `_. +If you're having problems, we recommend posting on the Let's Encrypt +`Community Forum `_. + +You can also chat with us on IRC: `(#certbot @ +OFTC) `_ or +`(#letsencrypt @ freenode) `_. If you find a bug in the software, please do report it in our `issue -tracker -`_. Remember to +tracker `_. Remember to give us as much information as possible: - copy and paste exact command line used and the output (though mind From a5df9e5a0e5fcb6a66c63e0bd02844417fdbb456 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 10 Oct 2016 18:44:39 -0700 Subject: [PATCH 202/331] Only verify required ports are available (#3608) * only verify port is available when you actually need it * refactor code to create achalls * Test port checks are based on achall * test that only the port for the requested challenge is checked in standalone --- certbot/plugins/standalone.py | 37 +++++++++++++++++++----------- certbot/plugins/standalone_test.py | 30 +++++++++++++++--------- tests/boulder-integration.sh | 13 +++++++++-- tests/integration/_common.sh | 8 ++++--- 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 0195b2726..e8c11a416 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -194,15 +194,6 @@ class Authenticator(common.Plugin): return [challenges.Challenge.TYPES[name] for name in self.conf("supported-challenges").split(",")] - @property - def _necessary_ports(self): - necessary_ports = set() - if challenges.HTTP01 in self.supported_challenges: - necessary_ports.add(self.config.http01_port) - if challenges.TLSSNI01 in self.supported_challenges: - necessary_ports.add(self.config.tls_sni_01_port) - return necessary_ports - def more_info(self): # pylint: disable=missing-docstring return("This authenticator creates its own ephemeral TCP listener " "on the necessary port in order to respond to incoming " @@ -217,12 +208,30 @@ class Authenticator(common.Plugin): # pylint: disable=unused-argument,missing-docstring return self.supported_challenges - def perform(self, achalls): # pylint: disable=missing-docstring - renewer = self.config.verb == "renew" - if any(util.already_listening(port, renewer) for port in self._necessary_ports): + def _verify_ports_are_available(self, achalls): + """Confirm the ports are available to solve all achalls. + + :param list achalls: list of + :class:`~certbot.achallenges.AnnotatedChallenge` + + :raises .errors.MisconfigurationError: if required port is + unavailable + + """ + ports = [] + if any(isinstance(ac.chall, challenges.HTTP01) for ac in achalls): + ports.append(self.config.http01_port) + if any(isinstance(ac.chall, challenges.TLSSNI01) for ac in achalls): + ports.append(self.config.tls_sni_01_port) + + renewer = (self.config.verb == "renew") + + if any(util.already_listening(port, renewer) for port in ports): raise errors.MisconfigurationError( - "At least one of the (possibly) required ports is " - "already taken.") + "At least one of the required ports is already taken.") + + def perform(self, achalls): # pylint: disable=missing-docstring + self._verify_ports_are_available(achalls) try: return self.perform2(achalls) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 1dfa3950a..56f842f68 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -137,20 +137,33 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual(self.auth.get_chall_pref(domain=None), [challenges.TLSSNI01]) + @classmethod + def _get_achalls(cls): + domain = b'localhost' + key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) + http_01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain=domain, account_key=key) + tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) + + return [http_01, tls_sni_01] + @mock.patch("certbot.plugins.standalone.util") def test_perform_already_listening(self, mock_util): - for chall, port in ((challenges.TLSSNI01.typ, 1234), - (challenges.HTTP01.typ, 4321)): + http_01, tls_sni_01 = self._get_achalls() + + for achall, port in ((http_01, self.config.http01_port,), + (tls_sni_01, self.config.tls_sni_01_port)): mock_util.already_listening.return_value = True - self.config.standalone_supported_challenges = chall self.assertRaises( - errors.MisconfigurationError, self.auth.perform, []) + errors.MisconfigurationError, self.auth.perform, [achall]) mock_util.already_listening.assert_called_once_with(port, False) mock_util.already_listening.reset_mock() @mock.patch("certbot.plugins.standalone.zope.component.getUtility") def test_perform(self, unused_mock_get_utility): - achalls = [1, 2, 3] + achalls = self._get_achalls() + self.auth.perform2 = mock.Mock(return_value=mock.sentinel.responses) self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls)) self.auth.perform2.assert_called_once_with(achalls) @@ -181,12 +194,7 @@ class AuthenticatorTest(unittest.TestCase): socket.errno.ENOTCONN, []) def test_perform2(self): - domain = b'localhost' - key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) - http_01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.HTTP01_P, domain=domain, account_key=key) - tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) + http_01, tls_sni_01 = self._get_achalls() self.auth.servers = mock.MagicMock() diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index ab8fde5f6..a70f13f8e 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -33,8 +33,17 @@ common() { "$@" } -common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth -common --domains le2.wtf --standalone-supported-challenges http-01 run +# We start a server listening on the port for the +# unrequested challenge to prevent regressions in #3601. +python -m SimpleHTTPServer $http_01_port & +python_server_pid=$! +common --domains le1.wtf --preferred-challenges tls-sni-01 auth +kill $python_server_pid +python -m SimpleHTTPServer $tls_sni_01_port & +python_server_pid=$! +common --domains le2.wtf --preferred-challenges http-01 run +kill $python_server_pid + common -a manual -d le.wtf auth --rsa-key-size 4096 export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 8992a18c0..935d44994 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -9,7 +9,9 @@ then fi store_flags="--config-dir $root/conf --work-dir $root/work" store_flags="$store_flags --logs-dir $root/logs" -export root store_flags +tls_sni_01_port=5001 +http_01_port=5002 +export root store_flags tls_sni_01_port http_01_port certbot_test () { certbot_test_no_force_renew \ @@ -21,8 +23,8 @@ certbot_test_no_force_renew () { certbot \ --server "${SERVER:-http://localhost:4000/directory}" \ --no-verify-ssl \ - --tls-sni-01-port 5001 \ - --http-01-port 5002 \ + --tls-sni-01-port $tls_sni_01_port \ + --http-01-port $http_01_port \ --manual-test-mode \ $store_flags \ --non-interactive \ From 4bc3c747cb39bca18580dc93ab697b35d9971ac9 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Mon, 10 Oct 2016 19:04:35 -0700 Subject: [PATCH 203/331] Mark parsed Nginx addresses as listening sslishly when an ssl on directive is included in the server block. (#3607) --- certbot-nginx/certbot_nginx/parser.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index a9ef21f2e..6203b5f71 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -473,6 +473,8 @@ def parse_server(server): 'ssl': False, 'names': set()} + apply_ssl_to_all_addrs = False + for directive in server: if not directive: continue @@ -486,6 +488,11 @@ def parse_server(server): _get_servernames(directive[1])) elif directive[0] == 'ssl' and directive[1] == 'on': parsed_server['ssl'] = True + apply_ssl_to_all_addrs = True + + if apply_ssl_to_all_addrs: + for addr in parsed_server['addrs']: + addr.ssl = True return parsed_server From e1da0efb8aef65e474a9ffa78c9c7fc01afb66f1 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Tue, 11 Oct 2016 12:22:58 -0700 Subject: [PATCH 204/331] Match psutil port open checking behavior to that of socket test, and update tests. (#3589) * Match psutil port open checking behavior to that of socket test, and update tests. * Update docstring --- certbot/plugins/standalone_test.py | 24 +++++++++++++++++------- certbot/plugins/util.py | 3 ++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 56f842f68..cb82ae7d8 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -106,13 +106,22 @@ class SupportedChallengesValidatorTest(unittest.TestCase): self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) +def get_open_port(): + """Gets an open port number from the OS.""" + open_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + open_socket.bind(("", 0)) + port = open_socket.getsockname()[1] + open_socket.close() + return port + class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.standalone.Authenticator.""" def setUp(self): from certbot.plugins.standalone import Authenticator + self.config = mock.MagicMock( - tls_sni_01_port=1234, http01_port=4321, + tls_sni_01_port=get_open_port(), http01_port=get_open_port(), standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") @@ -170,15 +179,16 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("certbot.plugins.standalone.zope.component.getUtility") def _test_perform_bind_errors(self, errno, achalls, mock_get_utility): + port = get_open_port() def _perform2(unused_achalls): - raise errors.StandaloneBindError(mock.Mock(errno=errno), 1234) + raise errors.StandaloneBindError(mock.Mock(errno=errno), port) self.auth.perform2 = mock.MagicMock(side_effect=_perform2) self.auth.perform(achalls) mock_get_utility.assert_called_once_with(interfaces.IDisplay) notification = mock_get_utility.return_value.notification self.assertEqual(1, notification.call_count) - self.assertTrue("1234" in notification.call_args[0][0]) + self.assertTrue(str(port) in notification.call_args[0][0]) def test_perform_eacces(self): # pylint: disable=no-value-for-parameter @@ -210,12 +220,12 @@ class AuthenticatorTest(unittest.TestCase): self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response)) self.assertEqual(self.auth.servers.run.mock_calls, [ - mock.call(4321, challenges.HTTP01), - mock.call(1234, challenges.TLSSNI01), + mock.call(self.config.http01_port, challenges.HTTP01), + mock.call(self.config.tls_sni_01_port, challenges.TLSSNI01), ]) self.assertEqual(self.auth.served, { - "server1234": set([tls_sni_01]), - "server4321": set([http_01]), + "server" + str(self.config.tls_sni_01_port): set([tls_sni_01]), + "server" + str(self.config.http01_port): set([http_01]), }) self.assertEqual(1, len(self.auth.http_01_resources)) self.assertEqual(1, len(self.auth.certs)) diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index b97ca1afd..0d0526fb9 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -123,7 +123,8 @@ def already_listening_psutil(port, renewer=False): return False listeners = [conn.pid for conn in net_connections - if conn.status == 'LISTEN' and + if (conn.status == 'LISTEN' or + conn.status == 'TIME_WAIT') and conn.type == socket.SOCK_STREAM and conn.laddr[1] == port] try: From f5bf66ba36d54a1362c91bee4d4f0c7bc800a055 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 11 Oct 2016 17:50:11 -0700 Subject: [PATCH 205/331] Check version requirements on optional dependencies (#3618) * Add and test activate function to acme. This function can be used to check if our optional dependencies are available and they meet our version requirements. * use activate in dns_resolver * use activate in dns_available() in challenges_test * Use activate in dns_resolver_test * Use activate in certbot.plugins.util_test * Use acme.util.activate for psutil * Better testing and handling of missing deps * Factored out *_available() code into a common function * Delayed exception caused from using acme.dns_resolver without dnspython until the function is called. This makes both production and testing code simpler. * Make a common subclass for already_listening tests * Simplify mocking of USE_PSUTIL in tests --- acme/acme/challenges.py | 7 +-- acme/acme/challenges_test.py | 27 +++++------- acme/acme/dns_resolver.py | 19 ++++++++- acme/acme/dns_resolver_test.py | 49 ++++++++++----------- acme/acme/test_util.py | 16 +++++++ acme/acme/util.py | 18 ++++++++ acme/acme/util_test.py | 18 ++++++++ certbot/plugins/util.py | 10 ++++- certbot/plugins/util_test.py | 78 ++++++++++++---------------------- certbot/tests/test_util.py | 16 +++++++ 10 files changed, 157 insertions(+), 101 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 4ebd37bf9..9f9cc05b8 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -9,6 +9,7 @@ from cryptography.hazmat.primitives import hashes import OpenSSL import requests +from acme import dns_resolver from acme import errors from acme import crypto_util from acme import fields @@ -232,11 +233,11 @@ class DNS01Response(KeyAuthorizationChallengeResponse): logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) try: - from acme import dns_resolver - except ImportError: # pragma: no cover + txt_records = dns_resolver.txt_records_for_name( + validation_domain_name) + except errors.DependencyError: raise errors.DependencyError("Local validation for 'dns-01' " "challenges requires 'dnspython'") - txt_records = dns_resolver.txt_records_for_name(validation_domain_name) exists = validation in txt_records if not exists: logger.debug("Key authorization from response (%r) doesn't match " diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index dfd40ebdb..5ac07abdd 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -10,6 +10,7 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err from acme import errors from acme import jose from acme import test_util +from acme.dns_resolver import DNS_REQUIREMENT CERT = test_util.load_comparable_cert('cert.pem') KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) @@ -76,20 +77,6 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase): self.assertFalse(response.verify(self.chall, KEY.public_key())) -def dns_available(): - """Checks if dns can be imported. - - :rtype: bool - :returns: ``True`` if dns can be imported, otherwise, ``False`` - - """ - try: - import dns # pylint: disable=unused-variable - except ImportError: # pragma: no cover - return False - return True # pragma: no cover - - class DNS01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes @@ -122,7 +109,13 @@ class DNS01ResponseTest(unittest.TestCase): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) - @test_util.skip_unless(dns_available(), + @mock.patch('acme.dns_resolver.DNS_AVAILABLE', False) + def test_simple_verify_without_dns(self): + self.assertRaises( + errors.DependencyError, self.response.simple_verify, + self.chall, 'local', KEY.public_key()) + + @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), "optional dependency dnspython is not available") def test_simple_verify_good_validation(self): # pragma: no cover with mock.patch(self.records_for_name_path) as mock_resolver: @@ -133,7 +126,7 @@ class DNS01ResponseTest(unittest.TestCase): mock_resolver.assert_called_once_with( self.chall.validation_domain_name("local")) - @test_util.skip_unless(dns_available(), + @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), "optional dependency dnspython is not available") def test_simple_verify_good_validation_multitxts(self): # pragma: no cover with mock.patch(self.records_for_name_path) as mock_resolver: @@ -144,7 +137,7 @@ class DNS01ResponseTest(unittest.TestCase): mock_resolver.assert_called_once_with( self.chall.validation_domain_name("local")) - @test_util.skip_unless(dns_available(), + @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), "optional dependency dnspython is not available") def test_simple_verify_bad_validation(self): # pragma: no cover with mock.patch(self.records_for_name_path) as mock_resolver: diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index f551c6095..2677d92ad 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -3,8 +3,20 @@ Required only for local validation of 'dns-01' challenges. """ import logging -import dns.resolver -import dns.exception +from acme import errors +from acme import util + +DNS_REQUIREMENT = 'dnspython>=1.12' + +try: + util.activate(DNS_REQUIREMENT) + # pragma: no cover + import dns.exception + import dns.resolver + DNS_AVAILABLE = True +except errors.DependencyError: # pragma: no cover + DNS_AVAILABLE = False + logger = logging.getLogger(__name__) @@ -18,6 +30,9 @@ def txt_records_for_name(name): :rtype: list of unicode """ + if not DNS_AVAILABLE: + raise errors.DependencyError( + '{0} is required to use this function'.format(DNS_REQUIREMENT)) try: dns_response = dns.resolver.query(name, 'TXT') except dns.resolver.NXDOMAIN as error: diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 03f1b3a93..2e2edd0e7 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,17 +1,16 @@ """Tests for acme.dns_resolver.""" -import sys import unittest import mock +from six.moves import reload_module # pylint: disable=import-error +from acme import errors from acme import test_util +from acme.dns_resolver import DNS_REQUIREMENT -try: +if test_util.requirement_available(DNS_REQUIREMENT): import dns - DNS_AVAILABLE = True # pragma: no cover -except ImportError: # pragma: no cover - DNS_AVAILABLE = False def create_txt_response(name, txt_records): @@ -25,15 +24,18 @@ def create_txt_response(name, txt_records): return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) -@test_util.skip_unless(DNS_AVAILABLE, - "optional dependency dnspython is not available") -class DnsResolverTestWithDns(unittest.TestCase): - """Tests for acme.dns_resolver when dns is available.""" +class TxtRecordsForNameTest(unittest.TestCase): + """Tests for acme.dns_resolver.txt_records_for_name.""" @classmethod - def _call(cls, name): - from acme import dns_resolver - return dns_resolver.txt_records_for_name(name) + def _call(cls, *args, **kwargs): + from acme.dns_resolver import txt_records_for_name + return txt_records_for_name(*args, **kwargs) + +@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), + "optional dependency dnspython is not available") +class TxtRecordsForNameWithDnsTest(TxtRecordsForNameTest): + """Tests for acme.dns_resolver.txt_records_for_name with dns.""" @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_with_single_response(self, mock_dns): mock_dns.return_value = create_txt_response('name', ['response']) @@ -56,24 +58,19 @@ class DnsResolverTestWithDns(unittest.TestCase): self.assertEquals([], self._call('name')) -class DnsResolverTestWithoutDns(unittest.TestCase): - """Tests for acme.dns_resolver when dns is unavailable.""" +class TxtRecordsForNameWithoutDnsTest(TxtRecordsForNameTest): + """Tests for acme.dns_resolver.txt_records_for_name without dns.""" def setUp(self): - self.dns_module = sys.modules['dns'] if 'dns' in sys.modules else None - - if DNS_AVAILABLE: - sys.modules['dns'] = None # pragma: no cover + from acme import dns_resolver + dns_resolver.DNS_AVAILABLE = False def tearDown(self): - if self.dns_module is not None: - sys.modules['dns'] = self.dns_module # pragma: no cover + from acme import dns_resolver + reload_module(dns_resolver) - @classmethod - def _import_dns(cls): - import dns as failed_dns_import # pylint: disable=unused-variable - - def test_import_error_is_raised(self): - self.assertRaises(ImportError, self._import_dns) + def test_exception_raised(self): + self.assertRaises( + errors.DependencyError, self._call, "example.org") if __name__ == '__main__': diff --git a/acme/acme/test_util.py b/acme/acme/test_util.py index 0f5763682..ba968511f 100644 --- a/acme/acme/test_util.py +++ b/acme/acme/test_util.py @@ -11,7 +11,9 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import OpenSSL +from acme import errors from acme import jose +from acme import util def vector_path(*names): @@ -76,6 +78,20 @@ def load_pyopenssl_private_key(*names): return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) +def requirement_available(requirement): + """Checks if requirement can be imported. + + :rtype: bool + :returns: ``True`` iff requirement can be imported + + """ + try: + util.activate(requirement) + except errors.DependencyError: # pragma: no cover + return False + return True # pragma: no cover + + def skip_unless(condition, reason): # pragma: no cover """Skip tests unless a condition holds. diff --git a/acme/acme/util.py b/acme/acme/util.py index 1fff89a9e..ac445b271 100644 --- a/acme/acme/util.py +++ b/acme/acme/util.py @@ -1,7 +1,25 @@ """ACME utilities.""" +import pkg_resources import six +from acme import errors + def map_keys(dikt, func): """Map dictionary keys.""" return dict((func(key), value) for key, value in six.iteritems(dikt)) + + +def activate(requirement): + """Make requirement importable. + + :param str requirement: the distribution and version to activate + + :raises acme.errors.DependencyError: if cannot activate requirement + + """ + try: + for distro in pkg_resources.require(requirement): # pylint: disable=not-callable + distro.activate() + except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict): + raise errors.DependencyError('{0} is unavailable'.format(requirement)) diff --git a/acme/acme/util_test.py b/acme/acme/util_test.py index 00aa8b02d..ba6465409 100644 --- a/acme/acme/util_test.py +++ b/acme/acme/util_test.py @@ -1,6 +1,8 @@ """Tests for acme.util.""" import unittest +from acme import errors + class MapKeysTest(unittest.TestCase): """Tests for acme.util.map_keys.""" @@ -12,5 +14,21 @@ class MapKeysTest(unittest.TestCase): self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1)) +class ActivateTest(unittest.TestCase): + """Tests for acme.util.activate.""" + + @classmethod + def _call(cls, *args, **kwargs): + from acme.util import activate + return activate(*args, **kwargs) + + def test_failure(self): + self.assertRaises(errors.DependencyError, self._call, 'acme>99.0.0') + + def test_success(self): + self._call('acme') + import acme as unused_acme + + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index 0d0526fb9..915b531c5 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -5,13 +5,19 @@ import socket import zope.component +from acme import errors as acme_errors +from acme import util as acme_util + from certbot import interfaces from certbot import util +PSUTIL_REQUIREMENT = "psutil>=2.2.1" + try: - import psutil + acme_util.activate(PSUTIL_REQUIREMENT) + import psutil # pragma: no cover USE_PSUTIL = True -except ImportError: +except acme_errors.DependencyError: # pragma: no cover USE_PSUTIL = False logger = logging.getLogger(__name__) diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index 71fb2a023..f8ffede86 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -1,11 +1,11 @@ """Tests for certbot.plugins.util.""" import os +import socket import unittest -import sys import mock -from six.moves import reload_module # pylint: disable=import-error +from certbot.plugins.util import PSUTIL_REQUIREMENT from certbot.tests import test_util @@ -34,71 +34,47 @@ class PathSurgeryTest(unittest.TestCase): self.assertTrue("/tmp" in os.environ["PATH"]) -class AlreadyListeningTestNoPsutil(unittest.TestCase): +class AlreadyListeningTest(unittest.TestCase): + """Tests for certbot.plugins.already_listening.""" + @classmethod + def _call(cls, *args, **kwargs): + from certbot.plugins.util import already_listening + return already_listening(*args, **kwargs) + + +class AlreadyListeningTestNoPsutil(AlreadyListeningTest): """Tests for certbot.plugins.already_listening when psutil is not available""" - def setUp(self): - import certbot.plugins.util - # Ensure we get importerror - self.psutil = None - if "psutil" in sys.modules: - self.psutil = sys.modules['psutil'] - sys.modules['psutil'] = None - # Reload hackery to ensure getting non-psutil version - # loaded to memory - reload_module(certbot.plugins.util) - - def tearDown(self): - # Need to reload the module to ensure - # getting back to normal - import certbot.plugins.util - sys.modules["psutil"] = self.psutil - reload_module(certbot.plugins.util) + @classmethod + def _call(cls, *args, **kwargs): + with mock.patch("certbot.plugins.util.USE_PSUTIL", False): + return super( + AlreadyListeningTestNoPsutil, cls)._call(*args, **kwargs) @mock.patch("certbot.plugins.util.zope.component.getUtility") def test_ports_available(self, mock_getutil): - import certbot.plugins.util as plugins_util # Ensure we don't get error with mock.patch("socket.socket.bind"): - self.assertFalse(plugins_util.already_listening(80)) - self.assertFalse(plugins_util.already_listening(80, True)) + self.assertFalse(self._call(80)) + self.assertFalse(self._call(80, True)) self.assertEqual(mock_getutil.call_count, 0) @mock.patch("certbot.plugins.util.zope.component.getUtility") def test_ports_blocked(self, mock_getutil): - sys.modules["psutil"] = None - import certbot.plugins.util as plugins_util - import socket - with mock.patch("socket.socket.bind", side_effect=socket.error): - self.assertTrue(plugins_util.already_listening(80)) - self.assertTrue(plugins_util.already_listening(80, True)) - with mock.patch("socket.socket", side_effect=socket.error): - self.assertFalse(plugins_util.already_listening(80)) + with mock.patch("certbot.plugins.util.socket.socket.bind") as mock_bind: + mock_bind.side_effect = socket.error + self.assertTrue(self._call(80)) + self.assertTrue(self._call(80, True)) + with mock.patch("certbot.plugins.util.socket.socket") as mock_socket: + mock_socket.side_effect = socket.error + self.assertFalse(self._call(80)) self.assertEqual(mock_getutil.call_count, 2) -def psutil_available(): - """Checks if psutil can be imported. - - :rtype: bool - :returns: ``True`` if psutil can be imported, otherwise, ``False`` - - """ - try: - import psutil # pylint: disable=unused-variable - except ImportError: - return False - return True - - -@test_util.skip_unless(psutil_available(), +@test_util.skip_unless(test_util.requirement_available(PSUTIL_REQUIREMENT), "optional dependency psutil is not available") -class AlreadyListeningTestPsutil(unittest.TestCase): +class AlreadyListeningTestPsutil(AlreadyListeningTest): """Tests for certbot.plugins.already_listening.""" - def _call(self, *args, **kwargs): - from certbot.plugins.util import already_listening - return already_listening(*args, **kwargs) - @mock.patch("certbot.plugins.util.psutil.net_connections") @mock.patch("certbot.plugins.util.psutil.Process") @mock.patch("certbot.plugins.util.zope.component.getUtility") diff --git a/certbot/tests/test_util.py b/certbot/tests/test_util.py index 0f5763682..ba968511f 100644 --- a/certbot/tests/test_util.py +++ b/certbot/tests/test_util.py @@ -11,7 +11,9 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import OpenSSL +from acme import errors from acme import jose +from acme import util def vector_path(*names): @@ -76,6 +78,20 @@ def load_pyopenssl_private_key(*names): return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) +def requirement_available(requirement): + """Checks if requirement can be imported. + + :rtype: bool + :returns: ``True`` iff requirement can be imported + + """ + try: + util.activate(requirement) + except errors.DependencyError: # pragma: no cover + return False + return True # pragma: no cover + + def skip_unless(condition, reason): # pragma: no cover """Skip tests unless a condition holds. From f008fd0af99c609a62250ff121c7aeb9eb813553 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 11 Oct 2016 19:15:11 -0700 Subject: [PATCH 206/331] Don't run nosetests from the root of our repo (#3620) --- tools/release.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/release.sh b/tools/release.sh index f5c78da27..57985d7a4 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -148,20 +148,22 @@ cd ~- # get a snapshot of the CLI help for the docs certbot --help all > docs/cli-help.txt +cd .. # freeze before installing anything else, so that we know end-user KGS # make sure "twine upload" doesn't catch "kgs" -if [ -d ../kgs ] ; then +if [ -d kgs ] ; then echo Deleting old kgs... - rm -rf ../kgs + rm -rf kgs fi -mkdir ../kgs -kgs="../kgs/$version" +mkdir kgs +kgs="kgs/$version" pip freeze | tee $kgs pip install nose for module in certbot $subpkgs_modules ; do echo testing $module nosetests $module done +cd ~- # pin pip hashes of the things we just built for pkg in acme certbot certbot-apache certbot-nginx ; do @@ -215,7 +217,6 @@ echo gpg -U $RELEASE_GPG_KEY --detach-sign --armor $name.$rev.tar.xz cd ~- echo "New root: $root" -echo "KGS is at $root/kgs" echo "Test commands (in the letstest repo):" echo 'python multitester.py targets.yaml $AWS_KEY $USERNAME scripts/test_leauto_upgrades.sh --alt_pip $YOUR_PIP_REPO --branch public-beta' echo 'python multitester.py targets.yaml $AWK_KEY $USERNAME scripts/test_letsencrypt_auto_certonly_standalone.sh --branch candidate-0.1.1' From 777356833221669ce578b53dc81a31100e0511aa Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 12 Oct 2016 14:46:02 -0700 Subject: [PATCH 207/331] Update ACME error namespace to match the new draft. (#3469) * Update error namespace in acme package. * Use new error namespace in certbot. * fix lint and py26 errors. * Update with_code docstring. * @pde's suggestions --- acme/acme/messages.py | 82 ++++++++++++++++++++++-------- acme/acme/messages_test.py | 29 ++++++++--- certbot/auth_handler.py | 10 ++-- certbot/client.py | 2 +- certbot/main.py | 12 +++-- certbot/tests/auth_handler_test.py | 2 +- certbot/tests/cli_test.py | 4 +- certbot/tests/client_test.py | 4 +- 8 files changed, 100 insertions(+), 45 deletions(-) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 563f80627..a7c86a10c 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -7,6 +7,37 @@ from acme import fields from acme import jose from acme import util +OLD_ERROR_PREFIX = "urn:acme:error:" +ERROR_PREFIX = "urn:ietf:params:acme:error:" + +ERROR_CODES = { + 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', + 'badNonce': 'The client sent an unacceptable anti-replay nonce', + 'connection': ('The server could not connect to the client to verify the' + ' domain'), + 'dnssec': 'The server could not validate a DNSSEC signed domain', + # deprecate invalidEmail + 'invalidEmail': 'The provided email for a registration was invalid', + 'invalidContact': 'The provided contact URI was invalid', + 'malformed': 'The request message was malformed', + 'rateLimited': 'There were too many requests of a given type', + 'serverInternal': 'The server experienced an internal error', + 'tls': 'The server experienced a TLS error during domain verification', + 'unauthorized': 'The client lacks sufficient authorization', + 'unknownHost': 'The server could not resolve a domain name', +} + +ERROR_TYPE_DESCRIPTIONS = dict( + (ERROR_PREFIX + name, desc) for name, desc in ERROR_CODES.items()) + +ERROR_TYPE_DESCRIPTIONS.update(dict( # add errors with old prefix, deprecate me + (OLD_ERROR_PREFIX + name, desc) for name, desc in ERROR_CODES.items())) + + +def is_acme_error(err): + """Check if argument is an ACME error.""" + return (ERROR_PREFIX in str(err)) or (OLD_ERROR_PREFIX in str(err)) + class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. @@ -18,31 +49,24 @@ class Error(jose.JSONObjectWithFields, errors.Error): :ivar unicode detail: """ - ERROR_TYPE_DESCRIPTIONS = dict( - ('urn:acme:error:' + name, description) for name, description in ( - ('badCSR', 'The CSR is unacceptable (e.g., due to a short key)'), - ('badNonce', 'The client sent an unacceptable anti-replay nonce'), - ('connection', 'The server could not connect to the client to ' - 'verify the domain'), - ('dnssec', 'The server could not validate a DNSSEC signed domain'), - ('invalidEmail', - 'The provided email for a registration was invalid'), - ('invalidContact', - 'The provided contact URI was invalid'), - ('malformed', 'The request message was malformed'), - ('rateLimited', 'There were too many requests of a given type'), - ('serverInternal', 'The server experienced an internal error'), - ('tls', 'The server experienced a TLS error during domain ' - 'verification'), - ('unauthorized', 'The client lacks sufficient authorization'), - ('unknownHost', 'The server could not resolve a domain name'), - ) - ) - typ = jose.Field('type', omitempty=True, default='about:blank') title = jose.Field('title', omitempty=True) detail = jose.Field('detail', omitempty=True) + @classmethod + def with_code(cls, code, **kwargs): + """Create an Error instance with an ACME Error code. + + :unicode code: An ACME error code, like 'dnssec'. + :kwargs: kwargs to pass to Error. + + """ + if code not in ERROR_CODES: + raise ValueError("The supplied code: %s is not a known ACME error" + " code" % code) + typ = ERROR_PREFIX + code + return cls(typ=typ, **kwargs) + @property def description(self): """Hardcoded error description based on its type. @@ -51,7 +75,21 @@ class Error(jose.JSONObjectWithFields, errors.Error): :rtype: unicode """ - return self.ERROR_TYPE_DESCRIPTIONS.get(self.typ) + return ERROR_TYPE_DESCRIPTIONS.get(self.typ) + + @property + def code(self): + """ACME error code. + + Basically self.typ without the ERROR_PREFIX. + + :returns: error code if standard ACME code or ``None``. + :rtype: unicode + + """ + code = str(self.typ).split(':')[-1] + if code in ERROR_CODES: + return code def __str__(self): return ' :: '.join( diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index 36d0dd618..a0322968c 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -17,13 +17,13 @@ class ErrorTest(unittest.TestCase): """Tests for acme.messages.Error.""" def setUp(self): - from acme.messages import Error + from acme.messages import Error, ERROR_PREFIX self.error = Error( - detail='foo', typ='urn:acme:error:malformed', title='title') + detail='foo', typ=ERROR_PREFIX + 'malformed', title='title') self.jobj = { 'detail': 'foo', 'title': 'some title', - 'type': 'urn:acme:error:malformed', + 'type': ERROR_PREFIX + 'malformed', } self.error_custom = Error(typ='custom', detail='bar') self.jobj_cusom = {'type': 'custom', 'detail': 'bar'} @@ -47,10 +47,27 @@ class ErrorTest(unittest.TestCase): def test_str(self): self.assertEqual( - 'urn:acme:error:malformed :: The request message was ' + 'urn:ietf:params:acme:error:malformed :: The request message was ' 'malformed :: foo :: title', str(self.error)) self.assertEqual('custom :: bar', str(self.error_custom)) + def test_code(self): + from acme.messages import Error + self.assertEqual('malformed', self.error.code) + self.assertEqual(None, self.error_custom.code) + self.assertEqual(None, Error().code) + + def test_is_acme_error(self): + from acme.messages import is_acme_error + self.assertTrue(is_acme_error(self.error)) + self.assertTrue(is_acme_error(str(self.error))) + self.assertFalse(is_acme_error(self.error_custom)) + + def test_with_code(self): + from acme.messages import Error, is_acme_error + self.assertTrue(is_acme_error(Error.with_code('badCSR'))) + self.assertRaises(ValueError, Error.with_code, 'not an ACME error code') + class ConstantTest(unittest.TestCase): """Tests for acme.messages._Constant.""" @@ -240,7 +257,7 @@ class ChallengeBodyTest(unittest.TestCase): from acme.messages import Error from acme.messages import STATUS_INVALID self.status = STATUS_INVALID - error = Error(typ='urn:acme:error:serverInternal', + error = Error(typ='urn:ietf:params:acme:error:serverInternal', detail='Unable to communicate with DNS server') self.challb = ChallengeBody( uri='http://challb', chall=self.chall, status=self.status, @@ -256,7 +273,7 @@ class ChallengeBodyTest(unittest.TestCase): self.jobj_from = self.jobj_to.copy() self.jobj_from['status'] = 'invalid' self.jobj_from['error'] = { - 'type': 'urn:acme:error:serverInternal', + 'type': 'urn:ietf:params:acme:error:serverInternal', 'detail': 'Unable to communicate with DNS server', } diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index cc8beb463..aad971eb6 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -437,9 +437,6 @@ def _report_no_chall_path(): raise errors.AuthorizationError(msg) -_ACME_PREFIX = "urn:acme:error:" - - _ERROR_HELP_COMMON = ( "To fix these errors, please make sure that your domain name was entered " "correctly and the DNS A record(s) for that domain contain(s) the " @@ -501,9 +498,10 @@ def _generate_failed_chall_msg(failed_achalls): :rtype: str """ - typ = failed_achalls[0].error.typ - if typ.startswith(_ACME_PREFIX): - typ = typ[len(_ACME_PREFIX):] + error = failed_achalls[0].error + typ = error.typ + if messages.is_acme_error(error): + typ = error.code msg = ["The following errors were reported by the server:"] for achall in failed_achalls: diff --git a/certbot/client.py b/certbot/client.py index 0c5d5ec59..55f3d5e67 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -149,7 +149,7 @@ def perform_registration(acme, config): try: return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error as e: - if e.typ == "urn:acme:error:invalidEmail" or e.typ == "urn:acme:error:invalidContact": + if e.code == "invalidEmail" or e.code == "invalidContact": if config.noninteractive_mode: msg = ("The ACME server believes %s is an invalid email address. " "Please ensure it is a valid email and attempt " diff --git a/certbot/main.py b/certbot/main.py index 35008fd62..5c8105ddd 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -12,6 +12,7 @@ import traceback import zope.component from acme import jose +from acme import messages import certbot @@ -691,11 +692,12 @@ def _handle_exception(exc_type, exc_value, trace, config): else: err = traceback.format_exception_only(exc_type, exc_value)[0] # Typical error from the ACME module: - # acme.messages.Error: urn:acme:error:malformed :: The request message was - # malformed :: Error creating new registration :: Validation of contact - # mailto:none@longrandomstring.biz failed: Server failure at resolver - if (("urn:acme" in err and ":: " in err and - config.verbose_count <= cli.flag_default("verbose_count"))): + # acme.messages.Error: urn:ietf:params:acme:error:malformed :: The + # request message was malformed :: Error creating new registration + # :: Validation of contact mailto:none@longrandomstring.biz failed: + # Server failure at resolver + if (messages.is_acme_error(err) and ":: " in err and + config.verbose_count <= cli.flag_default("verbose_count")): # prune ACME error code, we have a human description _code, _sep, err = err.partition(":: ") msg = "An unexpected error occurred:\n" + err + "Please see the " diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 84c3e16fa..9e0add196 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -386,7 +386,7 @@ class ReportFailedChallsTest(unittest.TestCase): "chall": acme_util.HTTP01, "uri": "uri", "status": messages.STATUS_INVALID, - "error": messages.Error(typ="urn:acme:error:tls", detail="detail"), + "error": messages.Error.with_code("tls", detail="detail"), } # Prevent future regressions if the error type changes diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index fde634c4c..3d17cc467 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -952,8 +952,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_sys.exit.assert_any_call(''.join( traceback.format_exception_only(errors.Error, error))) - exception = messages.Error(detail='alpha', typ='urn:acme:error:triffid', - title='beta') + bad_typ = messages.ERROR_PREFIX + 'triffid' + exception = messages.Error(detail='alpha', typ=bad_typ, title='beta') config = mock.MagicMock(debug=False, verbose_count=-3) main._handle_exception( messages.Error, exc_value=exception, trace=None, config=config) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 5e398c2cd..d61025116 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -65,7 +65,7 @@ class RegisterTest(unittest.TestCase): from acme import messages self.config.noninteractive_mode = False msg = "DNS problem: NXDOMAIN looking up MX for example.com" - mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidContact") + mx_err = messages.Error.with_code('invalidContact', detail=msg) with mock.patch("certbot.client.acme_client.Client") as mock_client: mock_client().register.side_effect = [mx_err, mock.MagicMock()] self._call() @@ -75,7 +75,7 @@ class RegisterTest(unittest.TestCase): def test_email_invalid_noninteractive(self, _rep): from acme import messages msg = "DNS problem: NXDOMAIN looking up MX for example.com" - mx_err = messages.Error(detail=msg, typ="urn:acme:error:invalidContact") + mx_err = messages.Error.with_code('invalidContact', detail=msg) with mock.patch("certbot.client.acme_client.Client") as mock_client: mock_client().register.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(errors.Error, self._call) From 8a925f20bbc219b6d51dededca17d21e2724149b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 7 Oct 2016 00:18:05 -0700 Subject: [PATCH 208/331] Document the Nginx plugin release (#3588) * Document the Nginx plugin release * Tweak * Remove mrueg nginx instructions for now? * Shipped -> included * keep order of plugin descriptions consistent with the table --- docs/using.rst | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 18dca071a..4604fd78f 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -179,21 +179,6 @@ want to use the Apache plugin, it has to be installed separately: emerge -av app-crypt/letsencrypt emerge -av app-crypt/letsencrypt-apache -Currently, only the Apache plugin is included in Portage. However, if you -Warning! -You can use Layman to add the mrueg overlay which does include a package for the -Certbot Nginx plugin, however, this plugin is known to be buggy and should only -be used with caution after creating a backup up your Nginx configuration. -We strongly recommend you use the app-crypt/letsencrypt package instead until -the Nginx plugin is ready. - -.. code-block:: shell - - emerge -av app-portage/layman - layman -S - layman -a mrueg - emerge -av app-crypt/letsencrypt-nginx - When using the Apache plugin, you will run into a "cannot find a cert or key directive" error if you're sporting the default Gentoo ``httpd.conf``. You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf`` @@ -272,13 +257,14 @@ apache_ Y Y | Automates obtaining and installing a cert with Apache 2. | Debian-based distributions with ``libaugeas0`` 1.0+. webroot_ Y N | Obtains a cert by writing to the webroot directory of an http-01_ (80) | already running webserver. +nginx_ Y Y | Automates obtaining and installing a cert with Nginx. Alpha tls-sni-01_ (443) + | release shipped with Certbot 0.9.0. standalone_ Y N | Uses a "standalone" webserver to obtain a cert. Requires http-01_ (80) or | port 80 or 443 to be available. This is useful on systems tls-sni-01_ (443) | with no webserver, or when direct integration with the local | webserver is not supported or not desired. manual_ Y N | Helps you obtain a cert by giving you instructions to perform http-01_ (80) or | domain validation yourself. dns-01_ (53) -nginx_ Y Y | Very experimental and not included in certbot-auto_. tls-sni-01_ (443) =========== ==== ==== =============================================================== ============================= Under the hood, plugins use one of several ACME protocol "Challenges_" to @@ -349,6 +335,19 @@ your webserver configuration, you might need to modify the configuration to ensure that files inside ``/.well-known/acme-challenge`` are served by the webserver. +Nginx +----- + +The Nginx plugin has been distributed with Cerbot since version 0.9.0 and should +work for most configurations. Because it is alpha code, we recommend backing up Nginx +configurations before using it (though you can also revert changes to +configurations with ``certbot --nginx rollback``). You can use it by providing +the ``--nginx`` flag on the commandline. + +:: + + certbot --nginx + Standalone ---------- @@ -378,15 +377,6 @@ the UI, you can use the plugin to obtain a cert by specifying to copy and paste commands into another terminal session, which may be on a different computer. -Nginx ------ - -In the future, if you're running Nginx you will hopefully be able to use this -plugin to automatically obtain and install your certificate. The Nginx plugin is -still experimental, however, and is not installed with certbot-auto_. If -installed, you can select this plugin on the command line by including -``--nginx``. - .. _third-party-plugins: Third-party plugins From 9d1a0b1d315ccf776c47569edf348c5ab478ac34 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 10 Oct 2016 13:17:49 -0700 Subject: [PATCH 209/331] Create symlinks at runtime and don't use relative paths (#3600) * Create symlinks at runtime in cli_test.py * use test_util.vector_path rather than hardcoding path * Reference #2716 in comment about too many lines in cli.py --- certbot/tests/cli_test.py | 65 ++++++++++++++----- .../testdata/live/sample-renewal/cert.pem | 1 - .../testdata/live/sample-renewal/chain.pem | 1 - .../live/sample-renewal/fullchain.pem | 1 - .../testdata/live/sample-renewal/privkey.pem | 1 - .../cert1.pem | 0 .../chain1.pem | 0 .../fullchain1.pem | 0 .../privkey1.pem | 0 .../testdata/sample-renewal-ancient.conf | 8 +-- 10 files changed, 52 insertions(+), 25 deletions(-) delete mode 120000 certbot/tests/testdata/live/sample-renewal/cert.pem delete mode 120000 certbot/tests/testdata/live/sample-renewal/chain.pem delete mode 120000 certbot/tests/testdata/live/sample-renewal/fullchain.pem delete mode 120000 certbot/tests/testdata/live/sample-renewal/privkey.pem rename certbot/tests/testdata/{archive/sample-renewal => sample-archive}/cert1.pem (100%) rename certbot/tests/testdata/{archive/sample-renewal => sample-archive}/chain1.pem (100%) rename certbot/tests/testdata/{archive/sample-renewal => sample-archive}/fullchain1.pem (100%) rename certbot/tests/testdata/{archive/sample-renewal => sample-archive}/privkey1.pem (100%) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 39a54258a..fde634c4c 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -1,4 +1,7 @@ """Tests for certbot.cli.""" +# Many tests in this file should be moved into +# main_test.py and renewal_test.py. See #2716. +# pylint: disable=too-many-lines from __future__ import print_function import argparse @@ -575,7 +578,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, args=None, should_renew=True, error_expected=False): # pylint: disable=too-many-locals,too-many-arguments - cert_path = 'certbot/tests/testdata/cert.pem' + cert_path = test_util.vector_path('cert.pem') chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path) mock_lineage.should_autorenew.return_value = due_for_renewal @@ -651,32 +654,60 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], log_out="not yet due", should_renew=False) - def _dump_log(self): with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: print("Logs:") print(lf.read()) + def _make_lineage(self, testfile): + """Creates a lineage defined by testfile. - def _make_test_renewal_conf(self, testfile): + This creates the archive, live, and renewal directories if + necessary and creates a simple lineage. + + :param str testfile: configuration file to base the lineage on + + :returns: path to the renewal conf file for the created lineage + :rtype: str + + """ + lineage_name = testfile[:-len('.conf')] + + conf_dir = os.path.join( + self.config_dir, constants.RENEWAL_CONFIGS_DIR) + archive_dir = os.path.join( + self.config_dir, constants.ARCHIVE_DIR, lineage_name) + live_dir = os.path.join( + self.config_dir, constants.LIVE_DIR, lineage_name) + + for directory in (archive_dir, conf_dir, live_dir,): + if not os.path.exists(directory): + os.makedirs(directory) + + sample_archive = test_util.vector_path('sample-archive') + for kind in os.listdir(sample_archive): + shutil.copyfile(os.path.join(sample_archive, kind), + os.path.join(archive_dir, kind)) + + for kind in storage.ALL_FOUR: + os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)), + os.path.join(live_dir, '{0}.pem'.format(kind))) + + conf_path = os.path.join(self.config_dir, conf_dir, testfile) with open(test_util.vector_path(testfile)) as src: - # put the correct path for cert.pem, chain.pem etc in the renewal conf - renewal_conf = src.read().replace("MAGICDIR", test_util.vector_path()) - rd = os.path.join(self.config_dir, "renewal") - if not os.path.exists(rd): - os.makedirs(rd) - rc = os.path.join(rd, "sample-renewal.conf") - with open(rc, "w") as dest: - dest.write(renewal_conf) - return rc + with open(conf_path, 'w') as dst: + dst.writelines( + line.replace('MAGICDIR', self.config_dir) for line in src) + + return conf_path def test_renew_verb(self): - self._make_test_renewal_conf('sample-renewal.conf') + self._make_lineage('sample-renewal.conf') args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, should_renew=True) def test_quiet_renew(self): - self._make_test_renewal_conf('sample-renewal.conf') + self._make_lineage('sample-renewal.conf') args = ["renew", "--dry-run"] _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) out = stdout.getvalue() @@ -688,13 +719,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual("", out) def test_renew_hook_validation(self): - self._make_test_renewal_conf('sample-renewal.conf') + self._make_lineage('sample-renewal.conf') args = ["renew", "--dry-run", "--post-hook=no-such-command"] self._test_renewal_common(True, [], args=args, should_renew=False, error_expected=True) def test_renew_no_hook_validation(self): - self._make_test_renewal_conf('sample-renewal.conf') + self._make_lineage('sample-renewal.conf') args = ["renew", "--dry-run", "--post-hook=no-such-command", "--disable-hook-validation"] self._test_renewal_common(True, [], args=args, should_renew=True, @@ -703,7 +734,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods @mock.patch("certbot.cli.set_by_cli") def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): mock_set_by_cli.return_value = False - rc_path = self._make_test_renewal_conf('sample-renewal-ancient.conf') + rc_path = self._make_lineage('sample-renewal-ancient.conf') args = mock.MagicMock(account=None, email=None, webroot_path=None) config = configuration.NamespaceConfig(args) lineage = storage.RenewableCert(rc_path, diff --git a/certbot/tests/testdata/live/sample-renewal/cert.pem b/certbot/tests/testdata/live/sample-renewal/cert.pem deleted file mode 120000 index e06effe40..000000000 --- a/certbot/tests/testdata/live/sample-renewal/cert.pem +++ /dev/null @@ -1 +0,0 @@ -../../archive/sample-renewal/cert1.pem \ No newline at end of file diff --git a/certbot/tests/testdata/live/sample-renewal/chain.pem b/certbot/tests/testdata/live/sample-renewal/chain.pem deleted file mode 120000 index 71f665f29..000000000 --- a/certbot/tests/testdata/live/sample-renewal/chain.pem +++ /dev/null @@ -1 +0,0 @@ -../../archive/sample-renewal/chain1.pem \ No newline at end of file diff --git a/certbot/tests/testdata/live/sample-renewal/fullchain.pem b/certbot/tests/testdata/live/sample-renewal/fullchain.pem deleted file mode 120000 index 0f06f077d..000000000 --- a/certbot/tests/testdata/live/sample-renewal/fullchain.pem +++ /dev/null @@ -1 +0,0 @@ -../../archive/sample-renewal/fullchain1.pem \ No newline at end of file diff --git a/certbot/tests/testdata/live/sample-renewal/privkey.pem b/certbot/tests/testdata/live/sample-renewal/privkey.pem deleted file mode 120000 index 5187eda6b..000000000 --- a/certbot/tests/testdata/live/sample-renewal/privkey.pem +++ /dev/null @@ -1 +0,0 @@ -../../archive/sample-renewal/privkey1.pem \ No newline at end of file diff --git a/certbot/tests/testdata/archive/sample-renewal/cert1.pem b/certbot/tests/testdata/sample-archive/cert1.pem similarity index 100% rename from certbot/tests/testdata/archive/sample-renewal/cert1.pem rename to certbot/tests/testdata/sample-archive/cert1.pem diff --git a/certbot/tests/testdata/archive/sample-renewal/chain1.pem b/certbot/tests/testdata/sample-archive/chain1.pem similarity index 100% rename from certbot/tests/testdata/archive/sample-renewal/chain1.pem rename to certbot/tests/testdata/sample-archive/chain1.pem diff --git a/certbot/tests/testdata/archive/sample-renewal/fullchain1.pem b/certbot/tests/testdata/sample-archive/fullchain1.pem similarity index 100% rename from certbot/tests/testdata/archive/sample-renewal/fullchain1.pem rename to certbot/tests/testdata/sample-archive/fullchain1.pem diff --git a/certbot/tests/testdata/archive/sample-renewal/privkey1.pem b/certbot/tests/testdata/sample-archive/privkey1.pem similarity index 100% rename from certbot/tests/testdata/archive/sample-renewal/privkey1.pem rename to certbot/tests/testdata/sample-archive/privkey1.pem diff --git a/certbot/tests/testdata/sample-renewal-ancient.conf b/certbot/tests/testdata/sample-renewal-ancient.conf index dd3075b8e..333bcaa18 100644 --- a/certbot/tests/testdata/sample-renewal-ancient.conf +++ b/certbot/tests/testdata/sample-renewal-ancient.conf @@ -1,7 +1,7 @@ -cert = MAGICDIR/live/sample-renewal/cert.pem -privkey = MAGICDIR/live/sample-renewal/privkey.pem -chain = MAGICDIR/live/sample-renewal/chain.pem -fullchain = MAGICDIR/live/sample-renewal/fullchain.pem +cert = MAGICDIR/live/sample-renewal-ancient/cert.pem +privkey = MAGICDIR/live/sample-renewal-ancient/privkey.pem +chain = MAGICDIR/live/sample-renewal-ancient/chain.pem +fullchain = MAGICDIR/live/sample-renewal-ancient/fullchain.pem renew_before_expiry = 1 year # Options and defaults used in the renewal process From e6686fbdb5ffe72bc19d64e81f52283b295cc99e Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 10 Oct 2016 18:36:58 -0700 Subject: [PATCH 210/331] Update Where Are My Certs section. (#3419) * Update Where Are My Certs section. This combines the `cert.pem` and `chain.pem` sections into a single paragraph, making it clearer that they are closely connected. It also adds text indicating that they are less common and moves them below the section for `fullchain.pem`. * Update "Getting Help" section. * Add link to document missing intermediate. * Remove incorrect line about ordering. Also remove "(as the filename suggests)," and clarify file ordering in the fullchain.pem section. --- docs/using.rst | 63 +++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 4604fd78f..d18d118cf 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -587,43 +587,41 @@ The following files are available: This is what Apache needs for `SSLCertificateKeyFile `_, - and nginx for `ssl_certificate_key + and Nginx for `ssl_certificate_key `_. -``cert.pem`` - Server certificate only. - - This is what Apache < 2.4.8 needs for `SSLCertificateFile - `_. - -``chain.pem`` - All certificates that need to be served by the browser **excluding** - server certificate, i.e. root and intermediate certificates only. - - This is what Apache < 2.4.8 needs for `SSLCertificateChainFile - `_, - and what nginx >= 1.3.7 needs for `ssl_trusted_certificate - `_. - ``fullchain.pem`` - All certificates, **including** server certificate. This is - concatenation of ``cert.pem`` and ``chain.pem``. + All certificates, **including** server certificate (aka leaf certificate or + end-entity certificate). The server certificate is the first one in this file, + followed by any intermediates. This is what Apache >= 2.4.8 needs for `SSLCertificateFile `_, - and what nginx needs for `ssl_certificate + and what Nginx needs for `ssl_certificate `_. +``cert.pem`` and ``chain.pem`` (less common) + ``cert.pem`` contains the server certificate by itself, and + ``chain.pem`` contains the additional intermediate certificate or + certificates that web browsers will need in order to validate the + server certificate. If you provide one of these files to your web + server, you **must** provide both of them, or some browsers will show + "This Connection is Untrusted" errors for your site, `some of the time + `_. -For both chain files, all certificates are ordered from root (primary -certificate) towards leaf. + Apache < 2.4.8 needs these for `SSLCertificateFile + `_. + and `SSLCertificateChainFile + `_, + respectively. -Please note, that **you must use** either ``chain.pem`` or -``fullchain.pem``. In case of webservers, using only ``cert.pem``, -will cause nasty errors served through the browsers! + If you're using OCSP stapling with Nginx >= 1.3.7, ``chain.pem`` should be + provided as the `ssl_trusted_certificate + `_ + to validate OCSP responses. -.. note:: All files are PEM-encoded (as the filename suffix - suggests). If you need other format, such as DER or PFX, then you +.. note:: All files are PEM-encoded. + If you need other format, such as DER or PFX, then you could convert using ``openssl``. You can automate that with ``--renew-hook`` if you're using automatic renewal_. @@ -653,14 +651,15 @@ By default, the following locations are searched: Getting help ============ -If you're having problems you can chat with us on `IRC (#certbot @ -OFTC) `_ or at -`IRC (#letsencrypt @ freenode) `_ -or get support on the Let's Encrypt `forums `_. +If you're having problems, we recommend posting on the Let's Encrypt +`Community Forum `_. + +You can also chat with us on IRC: `(#certbot @ +OFTC) `_ or +`(#letsencrypt @ freenode) `_. If you find a bug in the software, please do report it in our `issue -tracker -`_. Remember to +tracker `_. Remember to give us as much information as possible: - copy and paste exact command line used and the output (though mind From 54b36269cef419faac3413b23bf476a08edb2da3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 10 Oct 2016 18:44:39 -0700 Subject: [PATCH 211/331] Only verify required ports are available (#3608) * only verify port is available when you actually need it * refactor code to create achalls * Test port checks are based on achall * test that only the port for the requested challenge is checked in standalone --- certbot/plugins/standalone.py | 37 +++++++++++++++++++----------- certbot/plugins/standalone_test.py | 30 +++++++++++++++--------- tests/boulder-integration.sh | 13 +++++++++-- tests/integration/_common.sh | 8 ++++--- 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 0195b2726..e8c11a416 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -194,15 +194,6 @@ class Authenticator(common.Plugin): return [challenges.Challenge.TYPES[name] for name in self.conf("supported-challenges").split(",")] - @property - def _necessary_ports(self): - necessary_ports = set() - if challenges.HTTP01 in self.supported_challenges: - necessary_ports.add(self.config.http01_port) - if challenges.TLSSNI01 in self.supported_challenges: - necessary_ports.add(self.config.tls_sni_01_port) - return necessary_ports - def more_info(self): # pylint: disable=missing-docstring return("This authenticator creates its own ephemeral TCP listener " "on the necessary port in order to respond to incoming " @@ -217,12 +208,30 @@ class Authenticator(common.Plugin): # pylint: disable=unused-argument,missing-docstring return self.supported_challenges - def perform(self, achalls): # pylint: disable=missing-docstring - renewer = self.config.verb == "renew" - if any(util.already_listening(port, renewer) for port in self._necessary_ports): + def _verify_ports_are_available(self, achalls): + """Confirm the ports are available to solve all achalls. + + :param list achalls: list of + :class:`~certbot.achallenges.AnnotatedChallenge` + + :raises .errors.MisconfigurationError: if required port is + unavailable + + """ + ports = [] + if any(isinstance(ac.chall, challenges.HTTP01) for ac in achalls): + ports.append(self.config.http01_port) + if any(isinstance(ac.chall, challenges.TLSSNI01) for ac in achalls): + ports.append(self.config.tls_sni_01_port) + + renewer = (self.config.verb == "renew") + + if any(util.already_listening(port, renewer) for port in ports): raise errors.MisconfigurationError( - "At least one of the (possibly) required ports is " - "already taken.") + "At least one of the required ports is already taken.") + + def perform(self, achalls): # pylint: disable=missing-docstring + self._verify_ports_are_available(achalls) try: return self.perform2(achalls) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 1dfa3950a..56f842f68 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -137,20 +137,33 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual(self.auth.get_chall_pref(domain=None), [challenges.TLSSNI01]) + @classmethod + def _get_achalls(cls): + domain = b'localhost' + key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) + http_01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain=domain, account_key=key) + tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) + + return [http_01, tls_sni_01] + @mock.patch("certbot.plugins.standalone.util") def test_perform_already_listening(self, mock_util): - for chall, port in ((challenges.TLSSNI01.typ, 1234), - (challenges.HTTP01.typ, 4321)): + http_01, tls_sni_01 = self._get_achalls() + + for achall, port in ((http_01, self.config.http01_port,), + (tls_sni_01, self.config.tls_sni_01_port)): mock_util.already_listening.return_value = True - self.config.standalone_supported_challenges = chall self.assertRaises( - errors.MisconfigurationError, self.auth.perform, []) + errors.MisconfigurationError, self.auth.perform, [achall]) mock_util.already_listening.assert_called_once_with(port, False) mock_util.already_listening.reset_mock() @mock.patch("certbot.plugins.standalone.zope.component.getUtility") def test_perform(self, unused_mock_get_utility): - achalls = [1, 2, 3] + achalls = self._get_achalls() + self.auth.perform2 = mock.Mock(return_value=mock.sentinel.responses) self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls)) self.auth.perform2.assert_called_once_with(achalls) @@ -181,12 +194,7 @@ class AuthenticatorTest(unittest.TestCase): socket.errno.ENOTCONN, []) def test_perform2(self): - domain = b'localhost' - key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) - http_01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.HTTP01_P, domain=domain, account_key=key) - tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) + http_01, tls_sni_01 = self._get_achalls() self.auth.servers = mock.MagicMock() diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index ab8fde5f6..a70f13f8e 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -33,8 +33,17 @@ common() { "$@" } -common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth -common --domains le2.wtf --standalone-supported-challenges http-01 run +# We start a server listening on the port for the +# unrequested challenge to prevent regressions in #3601. +python -m SimpleHTTPServer $http_01_port & +python_server_pid=$! +common --domains le1.wtf --preferred-challenges tls-sni-01 auth +kill $python_server_pid +python -m SimpleHTTPServer $tls_sni_01_port & +python_server_pid=$! +common --domains le2.wtf --preferred-challenges http-01 run +kill $python_server_pid + common -a manual -d le.wtf auth --rsa-key-size 4096 export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 8992a18c0..935d44994 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -9,7 +9,9 @@ then fi store_flags="--config-dir $root/conf --work-dir $root/work" store_flags="$store_flags --logs-dir $root/logs" -export root store_flags +tls_sni_01_port=5001 +http_01_port=5002 +export root store_flags tls_sni_01_port http_01_port certbot_test () { certbot_test_no_force_renew \ @@ -21,8 +23,8 @@ certbot_test_no_force_renew () { certbot \ --server "${SERVER:-http://localhost:4000/directory}" \ --no-verify-ssl \ - --tls-sni-01-port 5001 \ - --http-01-port 5002 \ + --tls-sni-01-port $tls_sni_01_port \ + --http-01-port $http_01_port \ --manual-test-mode \ $store_flags \ --non-interactive \ From 4d6bf49393d3d219ade57dc492443e0ec08bec4e Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Mon, 10 Oct 2016 19:04:35 -0700 Subject: [PATCH 212/331] Mark parsed Nginx addresses as listening sslishly when an ssl on directive is included in the server block. (#3607) --- certbot-nginx/certbot_nginx/parser.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index a9ef21f2e..6203b5f71 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -473,6 +473,8 @@ def parse_server(server): 'ssl': False, 'names': set()} + apply_ssl_to_all_addrs = False + for directive in server: if not directive: continue @@ -486,6 +488,11 @@ def parse_server(server): _get_servernames(directive[1])) elif directive[0] == 'ssl' and directive[1] == 'on': parsed_server['ssl'] = True + apply_ssl_to_all_addrs = True + + if apply_ssl_to_all_addrs: + for addr in parsed_server['addrs']: + addr.ssl = True return parsed_server From 20ac4aebaf58459e26a8533db02c54d19926d2ad Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Tue, 11 Oct 2016 12:22:58 -0700 Subject: [PATCH 213/331] Match psutil port open checking behavior to that of socket test, and update tests. (#3589) * Match psutil port open checking behavior to that of socket test, and update tests. * Update docstring --- certbot/plugins/standalone_test.py | 24 +++++++++++++++++------- certbot/plugins/util.py | 3 ++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 56f842f68..cb82ae7d8 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -106,13 +106,22 @@ class SupportedChallengesValidatorTest(unittest.TestCase): self.assertEqual("tls-sni-01,http-01", self._call("dvsni,http-01")) +def get_open_port(): + """Gets an open port number from the OS.""" + open_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + open_socket.bind(("", 0)) + port = open_socket.getsockname()[1] + open_socket.close() + return port + class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.standalone.Authenticator.""" def setUp(self): from certbot.plugins.standalone import Authenticator + self.config = mock.MagicMock( - tls_sni_01_port=1234, http01_port=4321, + tls_sni_01_port=get_open_port(), http01_port=get_open_port(), standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") @@ -170,15 +179,16 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("certbot.plugins.standalone.zope.component.getUtility") def _test_perform_bind_errors(self, errno, achalls, mock_get_utility): + port = get_open_port() def _perform2(unused_achalls): - raise errors.StandaloneBindError(mock.Mock(errno=errno), 1234) + raise errors.StandaloneBindError(mock.Mock(errno=errno), port) self.auth.perform2 = mock.MagicMock(side_effect=_perform2) self.auth.perform(achalls) mock_get_utility.assert_called_once_with(interfaces.IDisplay) notification = mock_get_utility.return_value.notification self.assertEqual(1, notification.call_count) - self.assertTrue("1234" in notification.call_args[0][0]) + self.assertTrue(str(port) in notification.call_args[0][0]) def test_perform_eacces(self): # pylint: disable=no-value-for-parameter @@ -210,12 +220,12 @@ class AuthenticatorTest(unittest.TestCase): self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response)) self.assertEqual(self.auth.servers.run.mock_calls, [ - mock.call(4321, challenges.HTTP01), - mock.call(1234, challenges.TLSSNI01), + mock.call(self.config.http01_port, challenges.HTTP01), + mock.call(self.config.tls_sni_01_port, challenges.TLSSNI01), ]) self.assertEqual(self.auth.served, { - "server1234": set([tls_sni_01]), - "server4321": set([http_01]), + "server" + str(self.config.tls_sni_01_port): set([tls_sni_01]), + "server" + str(self.config.http01_port): set([http_01]), }) self.assertEqual(1, len(self.auth.http_01_resources)) self.assertEqual(1, len(self.auth.certs)) diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index b97ca1afd..0d0526fb9 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -123,7 +123,8 @@ def already_listening_psutil(port, renewer=False): return False listeners = [conn.pid for conn in net_connections - if conn.status == 'LISTEN' and + if (conn.status == 'LISTEN' or + conn.status == 'TIME_WAIT') and conn.type == socket.SOCK_STREAM and conn.laddr[1] == port] try: From 052be6d4bae07ea401e0ee4cd72a2862b9be5f46 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 11 Oct 2016 17:50:11 -0700 Subject: [PATCH 214/331] Check version requirements on optional dependencies (#3618) * Add and test activate function to acme. This function can be used to check if our optional dependencies are available and they meet our version requirements. * use activate in dns_resolver * use activate in dns_available() in challenges_test * Use activate in dns_resolver_test * Use activate in certbot.plugins.util_test * Use acme.util.activate for psutil * Better testing and handling of missing deps * Factored out *_available() code into a common function * Delayed exception caused from using acme.dns_resolver without dnspython until the function is called. This makes both production and testing code simpler. * Make a common subclass for already_listening tests * Simplify mocking of USE_PSUTIL in tests --- acme/acme/challenges.py | 7 +-- acme/acme/challenges_test.py | 27 +++++------- acme/acme/dns_resolver.py | 19 ++++++++- acme/acme/dns_resolver_test.py | 49 ++++++++++----------- acme/acme/test_util.py | 16 +++++++ acme/acme/util.py | 18 ++++++++ acme/acme/util_test.py | 18 ++++++++ certbot/plugins/util.py | 10 ++++- certbot/plugins/util_test.py | 78 ++++++++++++---------------------- certbot/tests/test_util.py | 16 +++++++ 10 files changed, 157 insertions(+), 101 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 4ebd37bf9..9f9cc05b8 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -9,6 +9,7 @@ from cryptography.hazmat.primitives import hashes import OpenSSL import requests +from acme import dns_resolver from acme import errors from acme import crypto_util from acme import fields @@ -232,11 +233,11 @@ class DNS01Response(KeyAuthorizationChallengeResponse): logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) try: - from acme import dns_resolver - except ImportError: # pragma: no cover + txt_records = dns_resolver.txt_records_for_name( + validation_domain_name) + except errors.DependencyError: raise errors.DependencyError("Local validation for 'dns-01' " "challenges requires 'dnspython'") - txt_records = dns_resolver.txt_records_for_name(validation_domain_name) exists = validation in txt_records if not exists: logger.debug("Key authorization from response (%r) doesn't match " diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index dfd40ebdb..5ac07abdd 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -10,6 +10,7 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err from acme import errors from acme import jose from acme import test_util +from acme.dns_resolver import DNS_REQUIREMENT CERT = test_util.load_comparable_cert('cert.pem') KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) @@ -76,20 +77,6 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase): self.assertFalse(response.verify(self.chall, KEY.public_key())) -def dns_available(): - """Checks if dns can be imported. - - :rtype: bool - :returns: ``True`` if dns can be imported, otherwise, ``False`` - - """ - try: - import dns # pylint: disable=unused-variable - except ImportError: # pragma: no cover - return False - return True # pragma: no cover - - class DNS01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes @@ -122,7 +109,13 @@ class DNS01ResponseTest(unittest.TestCase): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) - @test_util.skip_unless(dns_available(), + @mock.patch('acme.dns_resolver.DNS_AVAILABLE', False) + def test_simple_verify_without_dns(self): + self.assertRaises( + errors.DependencyError, self.response.simple_verify, + self.chall, 'local', KEY.public_key()) + + @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), "optional dependency dnspython is not available") def test_simple_verify_good_validation(self): # pragma: no cover with mock.patch(self.records_for_name_path) as mock_resolver: @@ -133,7 +126,7 @@ class DNS01ResponseTest(unittest.TestCase): mock_resolver.assert_called_once_with( self.chall.validation_domain_name("local")) - @test_util.skip_unless(dns_available(), + @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), "optional dependency dnspython is not available") def test_simple_verify_good_validation_multitxts(self): # pragma: no cover with mock.patch(self.records_for_name_path) as mock_resolver: @@ -144,7 +137,7 @@ class DNS01ResponseTest(unittest.TestCase): mock_resolver.assert_called_once_with( self.chall.validation_domain_name("local")) - @test_util.skip_unless(dns_available(), + @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), "optional dependency dnspython is not available") def test_simple_verify_bad_validation(self): # pragma: no cover with mock.patch(self.records_for_name_path) as mock_resolver: diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index f551c6095..2677d92ad 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -3,8 +3,20 @@ Required only for local validation of 'dns-01' challenges. """ import logging -import dns.resolver -import dns.exception +from acme import errors +from acme import util + +DNS_REQUIREMENT = 'dnspython>=1.12' + +try: + util.activate(DNS_REQUIREMENT) + # pragma: no cover + import dns.exception + import dns.resolver + DNS_AVAILABLE = True +except errors.DependencyError: # pragma: no cover + DNS_AVAILABLE = False + logger = logging.getLogger(__name__) @@ -18,6 +30,9 @@ def txt_records_for_name(name): :rtype: list of unicode """ + if not DNS_AVAILABLE: + raise errors.DependencyError( + '{0} is required to use this function'.format(DNS_REQUIREMENT)) try: dns_response = dns.resolver.query(name, 'TXT') except dns.resolver.NXDOMAIN as error: diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 03f1b3a93..2e2edd0e7 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,17 +1,16 @@ """Tests for acme.dns_resolver.""" -import sys import unittest import mock +from six.moves import reload_module # pylint: disable=import-error +from acme import errors from acme import test_util +from acme.dns_resolver import DNS_REQUIREMENT -try: +if test_util.requirement_available(DNS_REQUIREMENT): import dns - DNS_AVAILABLE = True # pragma: no cover -except ImportError: # pragma: no cover - DNS_AVAILABLE = False def create_txt_response(name, txt_records): @@ -25,15 +24,18 @@ def create_txt_response(name, txt_records): return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) -@test_util.skip_unless(DNS_AVAILABLE, - "optional dependency dnspython is not available") -class DnsResolverTestWithDns(unittest.TestCase): - """Tests for acme.dns_resolver when dns is available.""" +class TxtRecordsForNameTest(unittest.TestCase): + """Tests for acme.dns_resolver.txt_records_for_name.""" @classmethod - def _call(cls, name): - from acme import dns_resolver - return dns_resolver.txt_records_for_name(name) + def _call(cls, *args, **kwargs): + from acme.dns_resolver import txt_records_for_name + return txt_records_for_name(*args, **kwargs) + +@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), + "optional dependency dnspython is not available") +class TxtRecordsForNameWithDnsTest(TxtRecordsForNameTest): + """Tests for acme.dns_resolver.txt_records_for_name with dns.""" @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_with_single_response(self, mock_dns): mock_dns.return_value = create_txt_response('name', ['response']) @@ -56,24 +58,19 @@ class DnsResolverTestWithDns(unittest.TestCase): self.assertEquals([], self._call('name')) -class DnsResolverTestWithoutDns(unittest.TestCase): - """Tests for acme.dns_resolver when dns is unavailable.""" +class TxtRecordsForNameWithoutDnsTest(TxtRecordsForNameTest): + """Tests for acme.dns_resolver.txt_records_for_name without dns.""" def setUp(self): - self.dns_module = sys.modules['dns'] if 'dns' in sys.modules else None - - if DNS_AVAILABLE: - sys.modules['dns'] = None # pragma: no cover + from acme import dns_resolver + dns_resolver.DNS_AVAILABLE = False def tearDown(self): - if self.dns_module is not None: - sys.modules['dns'] = self.dns_module # pragma: no cover + from acme import dns_resolver + reload_module(dns_resolver) - @classmethod - def _import_dns(cls): - import dns as failed_dns_import # pylint: disable=unused-variable - - def test_import_error_is_raised(self): - self.assertRaises(ImportError, self._import_dns) + def test_exception_raised(self): + self.assertRaises( + errors.DependencyError, self._call, "example.org") if __name__ == '__main__': diff --git a/acme/acme/test_util.py b/acme/acme/test_util.py index 0f5763682..ba968511f 100644 --- a/acme/acme/test_util.py +++ b/acme/acme/test_util.py @@ -11,7 +11,9 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import OpenSSL +from acme import errors from acme import jose +from acme import util def vector_path(*names): @@ -76,6 +78,20 @@ def load_pyopenssl_private_key(*names): return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) +def requirement_available(requirement): + """Checks if requirement can be imported. + + :rtype: bool + :returns: ``True`` iff requirement can be imported + + """ + try: + util.activate(requirement) + except errors.DependencyError: # pragma: no cover + return False + return True # pragma: no cover + + def skip_unless(condition, reason): # pragma: no cover """Skip tests unless a condition holds. diff --git a/acme/acme/util.py b/acme/acme/util.py index 1fff89a9e..ac445b271 100644 --- a/acme/acme/util.py +++ b/acme/acme/util.py @@ -1,7 +1,25 @@ """ACME utilities.""" +import pkg_resources import six +from acme import errors + def map_keys(dikt, func): """Map dictionary keys.""" return dict((func(key), value) for key, value in six.iteritems(dikt)) + + +def activate(requirement): + """Make requirement importable. + + :param str requirement: the distribution and version to activate + + :raises acme.errors.DependencyError: if cannot activate requirement + + """ + try: + for distro in pkg_resources.require(requirement): # pylint: disable=not-callable + distro.activate() + except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict): + raise errors.DependencyError('{0} is unavailable'.format(requirement)) diff --git a/acme/acme/util_test.py b/acme/acme/util_test.py index 00aa8b02d..ba6465409 100644 --- a/acme/acme/util_test.py +++ b/acme/acme/util_test.py @@ -1,6 +1,8 @@ """Tests for acme.util.""" import unittest +from acme import errors + class MapKeysTest(unittest.TestCase): """Tests for acme.util.map_keys.""" @@ -12,5 +14,21 @@ class MapKeysTest(unittest.TestCase): self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1)) +class ActivateTest(unittest.TestCase): + """Tests for acme.util.activate.""" + + @classmethod + def _call(cls, *args, **kwargs): + from acme.util import activate + return activate(*args, **kwargs) + + def test_failure(self): + self.assertRaises(errors.DependencyError, self._call, 'acme>99.0.0') + + def test_success(self): + self._call('acme') + import acme as unused_acme + + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index 0d0526fb9..915b531c5 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -5,13 +5,19 @@ import socket import zope.component +from acme import errors as acme_errors +from acme import util as acme_util + from certbot import interfaces from certbot import util +PSUTIL_REQUIREMENT = "psutil>=2.2.1" + try: - import psutil + acme_util.activate(PSUTIL_REQUIREMENT) + import psutil # pragma: no cover USE_PSUTIL = True -except ImportError: +except acme_errors.DependencyError: # pragma: no cover USE_PSUTIL = False logger = logging.getLogger(__name__) diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index 71fb2a023..f8ffede86 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -1,11 +1,11 @@ """Tests for certbot.plugins.util.""" import os +import socket import unittest -import sys import mock -from six.moves import reload_module # pylint: disable=import-error +from certbot.plugins.util import PSUTIL_REQUIREMENT from certbot.tests import test_util @@ -34,71 +34,47 @@ class PathSurgeryTest(unittest.TestCase): self.assertTrue("/tmp" in os.environ["PATH"]) -class AlreadyListeningTestNoPsutil(unittest.TestCase): +class AlreadyListeningTest(unittest.TestCase): + """Tests for certbot.plugins.already_listening.""" + @classmethod + def _call(cls, *args, **kwargs): + from certbot.plugins.util import already_listening + return already_listening(*args, **kwargs) + + +class AlreadyListeningTestNoPsutil(AlreadyListeningTest): """Tests for certbot.plugins.already_listening when psutil is not available""" - def setUp(self): - import certbot.plugins.util - # Ensure we get importerror - self.psutil = None - if "psutil" in sys.modules: - self.psutil = sys.modules['psutil'] - sys.modules['psutil'] = None - # Reload hackery to ensure getting non-psutil version - # loaded to memory - reload_module(certbot.plugins.util) - - def tearDown(self): - # Need to reload the module to ensure - # getting back to normal - import certbot.plugins.util - sys.modules["psutil"] = self.psutil - reload_module(certbot.plugins.util) + @classmethod + def _call(cls, *args, **kwargs): + with mock.patch("certbot.plugins.util.USE_PSUTIL", False): + return super( + AlreadyListeningTestNoPsutil, cls)._call(*args, **kwargs) @mock.patch("certbot.plugins.util.zope.component.getUtility") def test_ports_available(self, mock_getutil): - import certbot.plugins.util as plugins_util # Ensure we don't get error with mock.patch("socket.socket.bind"): - self.assertFalse(plugins_util.already_listening(80)) - self.assertFalse(plugins_util.already_listening(80, True)) + self.assertFalse(self._call(80)) + self.assertFalse(self._call(80, True)) self.assertEqual(mock_getutil.call_count, 0) @mock.patch("certbot.plugins.util.zope.component.getUtility") def test_ports_blocked(self, mock_getutil): - sys.modules["psutil"] = None - import certbot.plugins.util as plugins_util - import socket - with mock.patch("socket.socket.bind", side_effect=socket.error): - self.assertTrue(plugins_util.already_listening(80)) - self.assertTrue(plugins_util.already_listening(80, True)) - with mock.patch("socket.socket", side_effect=socket.error): - self.assertFalse(plugins_util.already_listening(80)) + with mock.patch("certbot.plugins.util.socket.socket.bind") as mock_bind: + mock_bind.side_effect = socket.error + self.assertTrue(self._call(80)) + self.assertTrue(self._call(80, True)) + with mock.patch("certbot.plugins.util.socket.socket") as mock_socket: + mock_socket.side_effect = socket.error + self.assertFalse(self._call(80)) self.assertEqual(mock_getutil.call_count, 2) -def psutil_available(): - """Checks if psutil can be imported. - - :rtype: bool - :returns: ``True`` if psutil can be imported, otherwise, ``False`` - - """ - try: - import psutil # pylint: disable=unused-variable - except ImportError: - return False - return True - - -@test_util.skip_unless(psutil_available(), +@test_util.skip_unless(test_util.requirement_available(PSUTIL_REQUIREMENT), "optional dependency psutil is not available") -class AlreadyListeningTestPsutil(unittest.TestCase): +class AlreadyListeningTestPsutil(AlreadyListeningTest): """Tests for certbot.plugins.already_listening.""" - def _call(self, *args, **kwargs): - from certbot.plugins.util import already_listening - return already_listening(*args, **kwargs) - @mock.patch("certbot.plugins.util.psutil.net_connections") @mock.patch("certbot.plugins.util.psutil.Process") @mock.patch("certbot.plugins.util.zope.component.getUtility") diff --git a/certbot/tests/test_util.py b/certbot/tests/test_util.py index 0f5763682..ba968511f 100644 --- a/certbot/tests/test_util.py +++ b/certbot/tests/test_util.py @@ -11,7 +11,9 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import OpenSSL +from acme import errors from acme import jose +from acme import util def vector_path(*names): @@ -76,6 +78,20 @@ def load_pyopenssl_private_key(*names): return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) +def requirement_available(requirement): + """Checks if requirement can be imported. + + :rtype: bool + :returns: ``True`` iff requirement can be imported + + """ + try: + util.activate(requirement) + except errors.DependencyError: # pragma: no cover + return False + return True # pragma: no cover + + def skip_unless(condition, reason): # pragma: no cover """Skip tests unless a condition holds. From 1b65244d0d51c3fe4dd757782d098e67eca1672f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 11 Oct 2016 19:15:11 -0700 Subject: [PATCH 215/331] Don't run nosetests from the root of our repo (#3620) --- tools/release.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/release.sh b/tools/release.sh index f5c78da27..57985d7a4 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -148,20 +148,22 @@ cd ~- # get a snapshot of the CLI help for the docs certbot --help all > docs/cli-help.txt +cd .. # freeze before installing anything else, so that we know end-user KGS # make sure "twine upload" doesn't catch "kgs" -if [ -d ../kgs ] ; then +if [ -d kgs ] ; then echo Deleting old kgs... - rm -rf ../kgs + rm -rf kgs fi -mkdir ../kgs -kgs="../kgs/$version" +mkdir kgs +kgs="kgs/$version" pip freeze | tee $kgs pip install nose for module in certbot $subpkgs_modules ; do echo testing $module nosetests $module done +cd ~- # pin pip hashes of the things we just built for pkg in acme certbot certbot-apache certbot-nginx ; do @@ -215,7 +217,6 @@ echo gpg -U $RELEASE_GPG_KEY --detach-sign --armor $name.$rev.tar.xz cd ~- echo "New root: $root" -echo "KGS is at $root/kgs" echo "Test commands (in the letstest repo):" echo 'python multitester.py targets.yaml $AWS_KEY $USERNAME scripts/test_leauto_upgrades.sh --alt_pip $YOUR_PIP_REPO --branch public-beta' echo 'python multitester.py targets.yaml $AWK_KEY $USERNAME scripts/test_letsencrypt_auto_certonly_standalone.sh --branch candidate-0.1.1' From 6f808b6c082ce45fb0eec6ffa9018df668d334d7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 12 Oct 2016 16:12:29 -0700 Subject: [PATCH 216/331] Release 0.9.2 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 26 +++++++++--------- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto | 26 +++++++++--------- letsencrypt-auto-source/certbot-auto.asc | 14 +++++----- letsencrypt-auto-source/letsencrypt-auto | 26 +++++++++--------- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 24 ++++++++-------- 11 files changed, 63 insertions(+), 63 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 4bdfdefa0..9e84d3bcf 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.1' +version = '0.9.2' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index fdfdae395..2733e0398 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.1' +version = '0.9.2' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index eb627032f..22342d9d2 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.1" +LE_AUTO_VERSION="0.9.2" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.1 \ - --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ - --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 -certbot==0.9.1 \ - --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ - --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 -certbot-apache==0.9.1 \ - --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ - --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c -certbot-nginx==0.9.1 \ - --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ - --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c +acme==0.9.2 \ + --hash=sha256:730412573abf1ee64930d4e1233f02c12d9f7718bc43f127d7f16601ca134f45 \ + --hash=sha256:263d019b6bedd630d669442fad79bc62b4699b69fdb95a49a812fcf8e5bc82b4 +certbot==0.9.2 \ + --hash=sha256:ea096d3eb208798c04a3172fc8025fd4e4203940eac3d765c13546adc10ca28f \ + --hash=sha256:b2576bfe1295b2daab3d589ab9fcbd4d7a6928e85cea31a5e6b008e4d9a16869 +certbot-apache==0.9.2 \ + --hash=sha256:9a20fdbfd76bba5d8219c75c597eb1f576a1629f32fc201c77877871b7164c68 \ + --hash=sha256:92e504c5881b06e0abdc36020cfe57b3f7230e199c03e7da6728380af64e6c67 +certbot-nginx==0.9.2 \ + --hash=sha256:7883391110f05f9884cf62eb641bf1ae38cda07d91e631f9b44397a0d466304d \ + --hash=sha256:d6f7e66543a20991ef8ce11a37f926d00a9bbf5de61fe36bd6566e90c8b33b2f UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 9b4a7854d..d715c1533 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.1' +version = '0.9.2' install_requires = [ 'certbot', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index c434caef2..147bd8fce 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.1' +version = '0.9.2' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 0b082669f..5c571afab 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.9.1' +__version__ = '0.9.2' diff --git a/letsencrypt-auto b/letsencrypt-auto index eb627032f..22342d9d2 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.1" +LE_AUTO_VERSION="0.9.2" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.1 \ - --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ - --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 -certbot==0.9.1 \ - --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ - --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 -certbot-apache==0.9.1 \ - --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ - --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c -certbot-nginx==0.9.1 \ - --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ - --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c +acme==0.9.2 \ + --hash=sha256:730412573abf1ee64930d4e1233f02c12d9f7718bc43f127d7f16601ca134f45 \ + --hash=sha256:263d019b6bedd630d669442fad79bc62b4699b69fdb95a49a812fcf8e5bc82b4 +certbot==0.9.2 \ + --hash=sha256:ea096d3eb208798c04a3172fc8025fd4e4203940eac3d765c13546adc10ca28f \ + --hash=sha256:b2576bfe1295b2daab3d589ab9fcbd4d7a6928e85cea31a5e6b008e4d9a16869 +certbot-apache==0.9.2 \ + --hash=sha256:9a20fdbfd76bba5d8219c75c597eb1f576a1629f32fc201c77877871b7164c68 \ + --hash=sha256:92e504c5881b06e0abdc36020cfe57b3f7230e199c03e7da6728380af64e6c67 +certbot-nginx==0.9.2 \ + --hash=sha256:7883391110f05f9884cf62eb641bf1ae38cda07d91e631f9b44397a0d466304d \ + --hash=sha256:d6f7e66543a20991ef8ce11a37f926d00a9bbf5de61fe36bd6566e90c8b33b2f UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 71a54bde5..9ab7f4eac 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJX9shpAAoJEE0XyZXNl3XyeAUH/0928PysUZlLCQRpw3GPJnr0 -WgE1duULRfDKOdyoj8cIABEcxyK+rASyBju57Hx80Zuai9x4XSHJK7k9BXrZrU5k -KHZWbaNOKLN+C7/HTSOqGwalGTLglRJLZMwcj4rs8jtftg6GiWXvtnWuwqoiZJe4 -sCdddm2gu4D2VLp/QpBU6Gepuls4PmtB7SzwRUC6SAkWf5ntwJ4mq65bwKLcTPaZ -oRKoswo+eyiosH2SVVgiyAz7U96t2gxfK2pNoTdCUQGjuRaD6P2yBCdJA5h4L2l2 -W+/31zJpw/TgFErWpNuNYYoMh8cswaWXDNMUscsuduQ9KPLhSHQQ9JZ5f4w+9PM= -=Zhq9 +iQEcBAABAgAGBQJX/sM7AAoJEE0XyZXNl3XypJgIAJ2MsRr2BVq/ISAqpoGVEdTH +q2yp8xfOSnirYUvfYV9ZuwId06FlfC+9n2k0mT+byvl1HR4WFgX6f5//B17vTHCO +AnbxQIJ21SetzkR7XCKtW3A2GWs++4740diiv3Yrgn5VQkMWAJoy6k0NFgnED75R +cZsZPAYjX8zjCu4LKSrs6PuuNSA1Wj2E/q5fRN1/nFTP5uZhb+RrjT1RKo5i+pP9 +AjS96M8Dgw8ftPmtCkaK8f2btEQF2tDGbHPCrxiBbwMLKtrnAX+wwvkAYYjPqGlU +brF0+fZQ5spyJ8kqjBirLrCZ+kDmc7n7oKhlYluzLpx4cBYdV3CHeVZA6XG91Ag= +=MUCn -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index eb627032f..22342d9d2 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.1" +LE_AUTO_VERSION="0.9.2" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.1 \ - --hash=sha256:83e6188d5f149678b77ff3c8f8f94983f3c448490ffa634c7e9f2e93e7351fb0 \ - --hash=sha256:0effd08d144eedbfeb7dd784e9c8673ef3138cf48d35c8c33a1dd5bee9fd8207 -certbot==0.9.1 \ - --hash=sha256:88237a4f6d337d40185a644407f9c7adb6eab87c43b8bf56c1edc02ce82a9d81 \ - --hash=sha256:180354a3e95610ff8ba7f344011f2fcba1186d8efb7b25867266f47048e1e2f7 -certbot-apache==0.9.1 \ - --hash=sha256:cb9931294d44a1d6d44eaa2e92cb30daf6f2e0f29f6e7f00849709f0dd408b40 \ - --hash=sha256:1c09a6b4d087748f2aa143b0a7ced321458c3dd7ca70a80674f867b8b0e3cd6c -certbot-nginx==0.9.1 \ - --hash=sha256:3bd59a4f63a989eb31c04775107139bf45c2373a6f0b7bea4a3a1362da5c2ae7 \ - --hash=sha256:22f277846ccf5c8787cac2b35a7897780f05f4022de02d8b4b731c2cd957354c +acme==0.9.2 \ + --hash=sha256:730412573abf1ee64930d4e1233f02c12d9f7718bc43f127d7f16601ca134f45 \ + --hash=sha256:263d019b6bedd630d669442fad79bc62b4699b69fdb95a49a812fcf8e5bc82b4 +certbot==0.9.2 \ + --hash=sha256:ea096d3eb208798c04a3172fc8025fd4e4203940eac3d765c13546adc10ca28f \ + --hash=sha256:b2576bfe1295b2daab3d589ab9fcbd4d7a6928e85cea31a5e6b008e4d9a16869 +certbot-apache==0.9.2 \ + --hash=sha256:9a20fdbfd76bba5d8219c75c597eb1f576a1629f32fc201c77877871b7164c68 \ + --hash=sha256:92e504c5881b06e0abdc36020cfe57b3f7230e199c03e7da6728380af64e6c67 +certbot-nginx==0.9.2 \ + --hash=sha256:7883391110f05f9884cf62eb641bf1ae38cda07d91e631f9b44397a0d466304d \ + --hash=sha256:d6f7e66543a20991ef8ce11a37f926d00a9bbf5de61fe36bd6566e90c8b33b2f UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 80ec36e19ad2a8967d36cd409347726b08cdf0d7..4502c0195e9112f9e03a43c114680311917f395d 100644 GIT binary patch literal 256 zcmV+b0ssD7gd74~0IXvTSIAEi?JkusNAIoWu;kj87jg>TkokV&`NlD@Q00LjK||p@ z$CH!-_QznaqJ~6tcQYV`gn1;ZoW!}Mz8rV^f0lO~RxXgfmgSy3BYE2~O3qL?sF;h@ znb7Z=4kAV3ttTfHKc>0+Ivd(q%2O@`_ING0r*yClWU;K+FBTcmps1$5+;(G&cYsQ> zEm39J{-+C&rY}47LoP)~LzCyNe!_wjoLcv$8pb@&)nCR8ELcGH&xft!BhoVD@^EW8 z_BSbot`FNq(mPEpyIY$7manyW03fP^!+99Dj2Pk{L3ZU&5kbdXoR!DpSogzNm9Rq& G%NvQv?0=^K literal 256 zcmV+b0ssCA_-8=flSy<6Fl!QgcZZkvQ*|qkSu6D;yM{EED_69C0x#H)Ik$m9EG-?> zJ3eBlvyacp^n Date: Wed, 12 Oct 2016 16:12:35 -0700 Subject: [PATCH 217/331] Bump version to 0.10.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 9e84d3bcf..2b32f7e28 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.2' +version = '0.10.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 2733e0398..2b4ac8563 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.2' +version = '0.10.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index d715c1533..32e5935fb 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.2' +version = '0.10.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 147bd8fce..4c39d37c2 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.2' +version = '0.10.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 5c571afab..45892e269 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.9.2' +__version__ = '0.10.0.dev0' diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 22342d9d2..e95fdc357 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.2" +LE_AUTO_VERSION="0.10.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates From 3615b9030c91a324b03755e0a8902ca0936688c2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 12 Oct 2016 16:37:55 -0700 Subject: [PATCH 218/331] Improve description of what email is used for. Specifically, it's not currently used for account recovery. --- certbot/display/ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index ee570221f..662483ee0 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -29,7 +29,7 @@ def get_email(invalid=False, optional=True): """ invalid_prefix = "There seem to be problems with that address. " - msg = "Enter email address (used for urgent notices and lost key recovery)" + msg = "Enter email address (used for urgent renewal and security notices)" unsafe_suggestion = ("\n\nIf you really want to skip this, you can run " "the client with --register-unsafely-without-email " "but make sure you then backup your account key from " From 7f172859f554df0bd0d55333d19679ae6124dc62 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 12 Oct 2016 17:02:39 -0700 Subject: [PATCH 219/331] Nginx docs in README (#3606) * Update plugins docs in README - nginx is now part of certbot-auto - apache is now cross-platform * Alpha / beta * RST, not markdown --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 74dc870ed..244b6b510 100644 --- a/README.rst +++ b/README.rst @@ -152,11 +152,12 @@ Current Features * Supports multiple web servers: - - apache/2.x (working on Debian 8+ and Ubuntu 12.04+) - - standalone (runs its own simple webserver to prove you control a domain) + - apache/2.x (beta support for auto-configuration) + - nginx/0.8.48+ (alpha support for auto-configuration) - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - - nginx/0.8.48+ (highly experimental, not included in certbot-auto) + - standalone (runs its own simple webserver to prove you control a domain) + - other server software via `third party plugins `_ * The private key is generated locally on your system. * Can talk to the Let's Encrypt CA or optionally to other ACME From 77ed0c35ea2f05a367d82395bcec28901bef2606 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 13 Oct 2016 13:53:17 -0700 Subject: [PATCH 220/331] Match socket testing behavior to ACME standalone socket reuse behavior. Aggressively reuse ports, ignoring TIME_WAIT. (#3631) --- certbot/plugins/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index 915b531c5..786f6ca92 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -91,6 +91,7 @@ def already_listening_socket(port, renewer=False): try: testsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + testsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: testsocket.bind(("", port)) except socket.error: @@ -129,8 +130,7 @@ def already_listening_psutil(port, renewer=False): return False listeners = [conn.pid for conn in net_connections - if (conn.status == 'LISTEN' or - conn.status == 'TIME_WAIT') and + if conn.status == 'LISTEN' and conn.type == socket.SOCK_STREAM and conn.laddr[1] == port] try: From 6d0ba6de8e3151143e85c944fdbbb698f085fe37 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 13 Oct 2016 13:54:22 -0700 Subject: [PATCH 221/331] Fix Apache constants tests (#3630) * Allow running constants_test.py individually * Mock until tests pass Mock out both functions used to determine the OS in certbot_apache.tests.constants_test. --- .../certbot_apache/tests/constants_test.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/constants_test.py b/certbot-apache/certbot_apache/tests/constants_test.py index 1c842aee9..5ab324101 100644 --- a/certbot-apache/certbot_apache/tests/constants_test.py +++ b/certbot-apache/certbot_apache/tests/constants_test.py @@ -20,25 +20,25 @@ class ConstantsTest(unittest.TestCase): self.assertEqual(constants.os_constant("vhost_root"), "/etc/httpd/conf.d") + @mock.patch("certbot.util.get_systemd_os_like") @mock.patch("certbot.util.get_os_info") - def test_get_default_value(self, os_info): + def test_get_default_values(self, os_info, os_like): os_info.return_value = ('Nonexistent Linux', '', '') + os_like.return_value = {} + self.assertFalse(constants.os_constant("handle_mods")) + self.assertEqual(constants.os_constant("server_root"), "/etc/apache2") self.assertEqual(constants.os_constant("vhost_root"), "/etc/apache2/sites-available") + @mock.patch("certbot.util.get_systemd_os_like") @mock.patch("certbot.util.get_os_info") - def test_get_default_constants(self, os_info): + def test_get_darwin_like_values(self, os_info, os_like): os_info.return_value = ('Nonexistent Linux', '', '') - with mock.patch("certbot.util.get_systemd_os_like") as os_like: - # Get defaults - os_like.return_value = False - c_hm = constants.os_constant("handle_mods") - c_sr = constants.os_constant("server_root") - self.assertFalse(c_hm) - self.assertEqual(c_sr, "/etc/apache2") - # Use darwin as like test target - os_like.return_value = ["something", "nonexistent", "darwin"] - d_vr = constants.os_constant("vhost_root") - d_em = constants.os_constant("enmod") - self.assertFalse(d_em) - self.assertEqual(d_vr, "/etc/apache2/other") + os_like.return_value = ["something", "nonexistent", "darwin"] + self.assertFalse(constants.os_constant("enmod")) + self.assertEqual(constants.os_constant("vhost_root"), + "/etc/apache2/other") + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From 747a17d1faf69e3c6469bca70e43edfa71c8c0b4 Mon Sep 17 00:00:00 2001 From: Peter Conrad Date: Thu, 13 Oct 2016 16:13:17 -0700 Subject: [PATCH 222/331] Fixing a weird out-of-place paragraph in the Getting Certbot section (#3624) --- docs/using.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index d18d118cf..41e99f716 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -29,7 +29,12 @@ modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin. Getting Certbot =============== +Certbot is packaged for many common operating systems and web servers. Check whether +``certbot`` (or ``letsencrypt``) is packaged for your web server's OS by visiting +certbot.eff.org_, where you will also find the correct installation instructions for +your system. +.. Note:: Unless you have very specific requirements, we kindly suggest that you use the Certbot packages provided by your package manager (see certbot.eff.org_). If such packages are not available, we recommend using ``certbot-auto``, which automates the process of installing Certbot on your system. .. _certbot.eff.org: https://certbot.eff.org .. _certbot-auto: https://certbot.eff.org/docs/using.html#certbot-auto @@ -42,13 +47,6 @@ to, equivalently, as "subcommands") to request specific actions such as obtaining, renewing, or revoking certificates. Some of the most important and most commonly-used commands will be discussed throughout this document; an exhaustive list also appears near the end of the document. -======= -Certbot is packaged for many common operating systems and web servers. Check whether -``certbot`` (or ``letsencrypt``) is packaged for your web server's OS by visiting -certbot.eff.org_, where you will also find the correct installation instructions for -your system. - -.. Note:: Unless you have very specific requirements, we kindly suggest that you use the Certbot packages provided by your package manager (see certbot.eff.org_). If such packages are not available, we recommend using ``certbot-auto``, which automates the process of installing Certbot on your system. The ``certbot`` script on your web server might be named ``letsencrypt`` if your system uses an older package, or ``certbot-auto`` if you used an alternate installation method. Throughout the docs, whenever you see ``certbot``, swap in the correct name as needed. From 82ac89b850e77481028c1e055cda6cd01501f3a4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Sat, 15 Oct 2016 10:10:01 -0700 Subject: [PATCH 223/331] Release 0.9.3 Option 2 (see #3634) (#3635) * Release 0.9.3 (cherry picked from commit ce4e00569e6d8ed3d51c5a078d4281bec5f8e5f0) * Bump version to 0.10.0 (cherry picked from commit 5234172b8136fe74715315b32c1eda80411f6927) --- certbot-auto | 26 +++++++++--------- docs/cli-help.txt | 18 ++++++------ letsencrypt-auto | 26 +++++++++--------- letsencrypt-auto-source/certbot-auto.asc | 14 +++++----- letsencrypt-auto-source/letsencrypt-auto | 24 ++++++++-------- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 24 ++++++++-------- 7 files changed, 66 insertions(+), 66 deletions(-) diff --git a/certbot-auto b/certbot-auto index 22342d9d2..cba185eae 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.2" +LE_AUTO_VERSION="0.9.3" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.2 \ - --hash=sha256:730412573abf1ee64930d4e1233f02c12d9f7718bc43f127d7f16601ca134f45 \ - --hash=sha256:263d019b6bedd630d669442fad79bc62b4699b69fdb95a49a812fcf8e5bc82b4 -certbot==0.9.2 \ - --hash=sha256:ea096d3eb208798c04a3172fc8025fd4e4203940eac3d765c13546adc10ca28f \ - --hash=sha256:b2576bfe1295b2daab3d589ab9fcbd4d7a6928e85cea31a5e6b008e4d9a16869 -certbot-apache==0.9.2 \ - --hash=sha256:9a20fdbfd76bba5d8219c75c597eb1f576a1629f32fc201c77877871b7164c68 \ - --hash=sha256:92e504c5881b06e0abdc36020cfe57b3f7230e199c03e7da6728380af64e6c67 -certbot-nginx==0.9.2 \ - --hash=sha256:7883391110f05f9884cf62eb641bf1ae38cda07d91e631f9b44397a0d466304d \ - --hash=sha256:d6f7e66543a20991ef8ce11a37f926d00a9bbf5de61fe36bd6566e90c8b33b2f +acme==0.9.3 \ + --hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \ + --hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099 +certbot==0.9.3 \ + --hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \ + --hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0 +certbot-apache==0.9.3 \ + --hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \ + --hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086 +certbot-nginx==0.9.3 \ + --hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \ + --hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 7e321f407..f7340c48b 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -307,6 +307,15 @@ plugins: --webroot Obtain certs by placing files in a webroot directory. (default: False) +nginx: + Nginx Web Server plugin - Alpha + + --nginx-server-root NGINX_SERVER_ROOT + Nginx server root directory. (default: /etc/nginx) + --nginx-ctl NGINX_CTL + Path to the 'nginx' binary, used for 'configtest' and + retrieving nginx version number. (default: nginx) + standalone: Spin up a temporary webserver @@ -319,15 +328,6 @@ manual: Automatically allows public IP logging. (default: False) -nginx: - Nginx Web Server plugin - Alpha - - --nginx-server-root NGINX_SERVER_ROOT - Nginx server root directory. (default: /etc/nginx) - --nginx-ctl NGINX_CTL - Path to the 'nginx' binary, used for 'configtest' and - retrieving nginx version number. (default: nginx) - webroot: Place files in webroot directory diff --git a/letsencrypt-auto b/letsencrypt-auto index 22342d9d2..cba185eae 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.2" +LE_AUTO_VERSION="0.9.3" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.2 \ - --hash=sha256:730412573abf1ee64930d4e1233f02c12d9f7718bc43f127d7f16601ca134f45 \ - --hash=sha256:263d019b6bedd630d669442fad79bc62b4699b69fdb95a49a812fcf8e5bc82b4 -certbot==0.9.2 \ - --hash=sha256:ea096d3eb208798c04a3172fc8025fd4e4203940eac3d765c13546adc10ca28f \ - --hash=sha256:b2576bfe1295b2daab3d589ab9fcbd4d7a6928e85cea31a5e6b008e4d9a16869 -certbot-apache==0.9.2 \ - --hash=sha256:9a20fdbfd76bba5d8219c75c597eb1f576a1629f32fc201c77877871b7164c68 \ - --hash=sha256:92e504c5881b06e0abdc36020cfe57b3f7230e199c03e7da6728380af64e6c67 -certbot-nginx==0.9.2 \ - --hash=sha256:7883391110f05f9884cf62eb641bf1ae38cda07d91e631f9b44397a0d466304d \ - --hash=sha256:d6f7e66543a20991ef8ce11a37f926d00a9bbf5de61fe36bd6566e90c8b33b2f +acme==0.9.3 \ + --hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \ + --hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099 +certbot==0.9.3 \ + --hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \ + --hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0 +certbot-apache==0.9.3 \ + --hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \ + --hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086 +certbot-nginx==0.9.3 \ + --hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \ + --hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 9ab7f4eac..db40cfb84 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJX/sM7AAoJEE0XyZXNl3XypJgIAJ2MsRr2BVq/ISAqpoGVEdTH -q2yp8xfOSnirYUvfYV9ZuwId06FlfC+9n2k0mT+byvl1HR4WFgX6f5//B17vTHCO -AnbxQIJ21SetzkR7XCKtW3A2GWs++4740diiv3Yrgn5VQkMWAJoy6k0NFgnED75R -cZsZPAYjX8zjCu4LKSrs6PuuNSA1Wj2E/q5fRN1/nFTP5uZhb+RrjT1RKo5i+pP9 -AjS96M8Dgw8ftPmtCkaK8f2btEQF2tDGbHPCrxiBbwMLKtrnAX+wwvkAYYjPqGlU -brF0+fZQ5spyJ8kqjBirLrCZ+kDmc7n7oKhlYluzLpx4cBYdV3CHeVZA6XG91Ag= -=MUCn +iQEcBAABAgAGBQJYADL6AAoJEE0XyZXNl3XyZW8H/RgPxga4SZ8VoMGGOpzYGzaD +C/VW6IZeHjD7urkAjfSiMMStkYKlZMGcT/3Pw1L39wIX/37jqQTTh01JL+TcqRMJ +AUHmSgrErjUU42YV68u2c/wT9Dsid+OxpP/WSbJn5MomWtvGpFxffc/FK/W8ccFR +r6ZhAt2rgkBmYjrC6w8V9KTzhp4+n7ZpQPxuMFxpJhyTmMzgj9K+aI2OuKDKT7iO +nke74Lgx/xPatLDgygw5bRiFyZ+X65p/awalEXBcFW0zmlN2Fqp8om8UjtUtkVw9 +ixr9/kq9VhcHjho9cmKWl14IShbcxZZc60xL2y6gmkgoBpzVlHfvRNnxapodTsc= +=jULW -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index e95fdc357..b5adeea86 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -761,18 +761,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.2 \ - --hash=sha256:730412573abf1ee64930d4e1233f02c12d9f7718bc43f127d7f16601ca134f45 \ - --hash=sha256:263d019b6bedd630d669442fad79bc62b4699b69fdb95a49a812fcf8e5bc82b4 -certbot==0.9.2 \ - --hash=sha256:ea096d3eb208798c04a3172fc8025fd4e4203940eac3d765c13546adc10ca28f \ - --hash=sha256:b2576bfe1295b2daab3d589ab9fcbd4d7a6928e85cea31a5e6b008e4d9a16869 -certbot-apache==0.9.2 \ - --hash=sha256:9a20fdbfd76bba5d8219c75c597eb1f576a1629f32fc201c77877871b7164c68 \ - --hash=sha256:92e504c5881b06e0abdc36020cfe57b3f7230e199c03e7da6728380af64e6c67 -certbot-nginx==0.9.2 \ - --hash=sha256:7883391110f05f9884cf62eb641bf1ae38cda07d91e631f9b44397a0d466304d \ - --hash=sha256:d6f7e66543a20991ef8ce11a37f926d00a9bbf5de61fe36bd6566e90c8b33b2f +acme==0.9.3 \ + --hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \ + --hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099 +certbot==0.9.3 \ + --hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \ + --hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0 +certbot-apache==0.9.3 \ + --hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \ + --hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086 +certbot-nginx==0.9.3 \ + --hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \ + --hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 4502c0195e9112f9e03a43c114680311917f395d..f3950b7d64ea779f03f935b957936a5273e20767 100644 GIT binary patch literal 256 zcmV+b0ssDRCvaD}1PJYxJ7X8ROr?`s(?BE!MRwbUu@HJmfG_K(FD%Jlo=!*v6>VHs zStR_UX!Ns;l*829-?y0LzMsj5ag3>P^-a+az`q84VnGD0L*{==#Vow+-<#g7CkT)A zW+lbL0F>^blSiI(QZDV4yYWBo_c^d8Sas}l}uktDA-T_TkokV&`NlD@Q00LjK||p@ z$CH!-_QznaqJ~6tcQYV`gn1;ZoW!}Mz8rV^f0lO~RxXgfmgSy3BYE2~O3qL?sF;h@ znb7Z=4kAV3ttTfHKc>0+Ivd(q%2O@`_ING0r*yClWU;K+FBTcmps1$5+;(G&cYsQ> zEm39J{-+C&rY}47LoP)~LzCyNe!_wjoLcv$8pb@&)nCR8ELcGH&xft!BhoVD@^EW8 z_BSbot`FNq(mPEpyIY$7manyW03fP^!+99Dj2Pk{L3ZU&5kbdXoR!DpSogzNm9Rq& G%NvQv?0=^K diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index ccadb799e..1803d51b8 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -170,15 +170,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.9.2 \ - --hash=sha256:730412573abf1ee64930d4e1233f02c12d9f7718bc43f127d7f16601ca134f45 \ - --hash=sha256:263d019b6bedd630d669442fad79bc62b4699b69fdb95a49a812fcf8e5bc82b4 -certbot==0.9.2 \ - --hash=sha256:ea096d3eb208798c04a3172fc8025fd4e4203940eac3d765c13546adc10ca28f \ - --hash=sha256:b2576bfe1295b2daab3d589ab9fcbd4d7a6928e85cea31a5e6b008e4d9a16869 -certbot-apache==0.9.2 \ - --hash=sha256:9a20fdbfd76bba5d8219c75c597eb1f576a1629f32fc201c77877871b7164c68 \ - --hash=sha256:92e504c5881b06e0abdc36020cfe57b3f7230e199c03e7da6728380af64e6c67 -certbot-nginx==0.9.2 \ - --hash=sha256:7883391110f05f9884cf62eb641bf1ae38cda07d91e631f9b44397a0d466304d \ - --hash=sha256:d6f7e66543a20991ef8ce11a37f926d00a9bbf5de61fe36bd6566e90c8b33b2f +acme==0.9.3 \ + --hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \ + --hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099 +certbot==0.9.3 \ + --hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \ + --hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0 +certbot-apache==0.9.3 \ + --hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \ + --hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086 +certbot-nginx==0.9.3 \ + --hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \ + --hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058 From 91deb6ec531958f39d5a076c20ae7740f61ac68d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 17 Oct 2016 13:11:24 -0700 Subject: [PATCH 224/331] Add test_tests.sh (#3633) --- tests/letstest/scripts/test_tests.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100755 tests/letstest/scripts/test_tests.sh diff --git a/tests/letstest/scripts/test_tests.sh b/tests/letstest/scripts/test_tests.sh new file mode 100755 index 000000000..10cad2549 --- /dev/null +++ b/tests/letstest/scripts/test_tests.sh @@ -0,0 +1,17 @@ +#!/bin/sh -xe + +MODULES="acme certbot certbot_apache certbot_nginx" +VENV_NAME=venv + +# *-auto respects VENV_PATH +VENV_PATH=$VENV_NAME letsencrypt/certbot-auto --debug --non-interactive --version +. $VENV_NAME/bin/activate + +# change to an empty directory to ensure CWD doesn't affect tests +cd $(mktemp -d) +pip install nose + +for module in $MODULES ; do + echo testing $module + nosetests -v $module +done From 605a3cc931ff815e908282d77d0edeebd9ec94f6 Mon Sep 17 00:00:00 2001 From: schoen Date: Mon, 17 Oct 2016 19:48:48 -0700 Subject: [PATCH 225/331] Stop rejecting punycode domain names (#3626) * Punycode is about to be permitted; stop rejecting it * Remove spurious bracket * More brackets rather than fewer! * Change ops_test's notion of valid domains * Remove spurious "certonly" from new test * Make test more localized * Remove commented-out punycode prohibition --- certbot/tests/cli_test.py | 9 +++++---- certbot/tests/display/ops_test.py | 8 ++------ certbot/util.py | 6 ------ 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 3d17cc467..5d2051cfe 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -346,11 +346,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods except errors.PluginSelectionError as e: self.assertTrue('The requested bad_auth plugin does not appear' in str(e)) + def test_punycode_ok(self): + # Punycode is now legal, so no longer an error; instead check + # that it's _not_ an error (at the initial sanity check stage) + util.enforce_domain_sanity('this.is.xn--ls8h.tld') + def test_check_config_sanity_domain(self): - # Punycode - self.assertRaises(errors.ConfigurationError, - self._call, - ['-d', 'this.is.xn--ls8h.tld']) # FQDN self.assertRaises(errors.ConfigurationError, self._call, diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 2e3e65261..bc0696f9c 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -244,8 +244,8 @@ class ChooseNamesTest(unittest.TestCase): all_valid = ["example.com", "second.example.com", "also.example.com", "under_score.example.com", "justtld"] - all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "uniçodé.com"] - two_valid = ["example.com", "xn--ls8h.tld", "also.example.com"] + all_invalid = ["öóòps.net", "*.wildcard.com", "uniçodé.com"] + two_valid = ["example.com", "úniçøde.com", "also.example.com"] self.assertEqual(get_valid_domains(all_valid), all_valid) self.assertEqual(get_valid_domains(all_invalid), []) self.assertEqual(len(get_valid_domains(two_valid)), 2) @@ -266,10 +266,6 @@ class ChooseNamesTest(unittest.TestCase): unicode_error = UnicodeEncodeError('mock', u'', 0, 1, 'mock') mock_sli.side_effect = unicode_error self.assertEqual(_choose_names_manually(), []) - # Punycode and no retry - mock_util().input.return_value = (display_util.OK, - "xn--ls8h.tld") - self.assertEqual(_choose_names_manually(), []) # Valid domains mock_util().input.return_value = (display_util.OK, ("example.com," diff --git a/certbot/util.py b/certbot/util.py index 5cb4b4cad..577180b00 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -437,19 +437,13 @@ def enforce_domain_sanity(domain): """ if isinstance(domain, six.text_type): wildcard_marker = u"*." - punycode_marker = u"xn--" else: wildcard_marker = b"*." - punycode_marker = b"xn--" # Check if there's a wildcard domain if domain.startswith(wildcard_marker): raise errors.ConfigurationError( "Wildcard domains are not supported: {0}".format(domain)) - # Punycode - if punycode_marker in domain: - raise errors.ConfigurationError( - "Punycode domains are not presently supported: {0}".format(domain)) # Unicode try: From b9adb7cbaf6c9151f844ce8cc0dd7e043f273a37 Mon Sep 17 00:00:00 2001 From: benbankes Date: Wed, 19 Oct 2016 09:53:46 -0600 Subject: [PATCH 226/331] Fix typo (#3659) --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 41e99f716..57589349b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -54,7 +54,7 @@ The ``certbot`` script on your web server might be named ``letsencrypt`` if your Other installation methods -------------------------- If you are offline or your operating system doesn't provide a package, you can use -an alternate method fo install ``certbot``. +an alternate method for installing ``certbot``. Certbot-Auto ^^^^^^^^^^^^ From ce252bd6c901f1dd1e7848b09c9b3661fcaecf80 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Fri, 21 Oct 2016 13:56:53 -0700 Subject: [PATCH 227/331] Allow certbot to get a cert for default_servers (#3652) * Allow certbot to get a cert for default_servers * Add to_string method for not printing default_server --- certbot-nginx/certbot_nginx/obj.py | 8 ++++++-- .../certbot_nginx/tests/configurator_test.py | 6 ++++-- certbot-nginx/certbot_nginx/tests/obj_test.py | 10 ++++++++++ certbot-nginx/certbot_nginx/tests/parser_test.py | 11 +++++++---- .../testdata/etc_nginx/sites-enabled/sslon.com | 6 ++++++ .../certbot_nginx/tests/tls_sni_01_test.py | 13 +++++++++---- certbot-nginx/certbot_nginx/tls_sni_01.py | 13 +++++-------- 7 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/sslon.com diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 8c93d0a8b..c58a82450 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -69,7 +69,8 @@ class Addr(common.Addr): return cls(host, port, ssl, default) - def __str__(self): + def to_string(self, include_default=True): + """Return string representation of Addr""" parts = '' if self.tup[0] and self.tup[1]: parts = "%s:%s" % self.tup @@ -78,13 +79,16 @@ class Addr(common.Addr): else: parts = self.tup[1] - if self.default: + if self.default and include_default: parts += ' default_server' if self.ssl: parts += ' ssl' return parts + def __str__(self): + return self.to_string() + def __repr__(self): return "Addr(" + self.__str__() + ")" diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 10f5e5514..d871a5720 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -40,7 +40,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_prepare(self): self.assertEqual((1, 6, 2), self.config.version) - self.assertEqual(6, len(self.config.parser.parsed)) + self.assertEqual(7, len(self.config.parser.parsed)) # ensure we successfully parsed a file for ssl_options self.assertTrue(self.config.parser.loc["ssl_options"]) @@ -68,7 +68,7 @@ class NginxConfiguratorTest(util.NginxTest): names = self.config.get_all_names() self.assertEqual(names, set( ["155.225.50.69.nephoscale.net", "www.example.org", "another.alias", - "migration.com", "summer.com", "geese.com"])) + "migration.com", "summer.com", "geese.com", "sslon.com"])) def test_supported_enhancements(self): self.assertEqual(['redirect', 'staple-ocsp'], @@ -242,6 +242,7 @@ class NginxConfiguratorTest(util.NginxTest): nginx_conf = self.config.parser.abs_path('nginx.conf') example_conf = self.config.parser.abs_path('sites-enabled/example.com') migration_conf = self.config.parser.abs_path('sites-enabled/migration.com') + sslon_conf = self.config.parser.abs_path('sites-enabled/sslon.com') # Get the default SSL vhost self.config.deploy_cert( @@ -269,6 +270,7 @@ class NginxConfiguratorTest(util.NginxTest): ('example/fullchain.pem', 'example/key.pem', example_conf), ('/etc/nginx/fullchain.pem', '/etc/nginx/key.pem', nginx_conf), ('migration/fullchain.pem', 'migration/key.pem', migration_conf), + ('snakeoil.cert', 'snakeoil.key', sslon_conf), ]), self.config.get_all_certs_keys()) @mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform") diff --git a/certbot-nginx/certbot_nginx/tests/obj_test.py b/certbot-nginx/certbot_nginx/tests/obj_test.py index 200f2acb9..84d0c6bca 100644 --- a/certbot-nginx/certbot_nginx/tests/obj_test.py +++ b/certbot-nginx/certbot_nginx/tests/obj_test.py @@ -55,6 +55,16 @@ class AddrTest(unittest.TestCase): self.assertEqual(str(self.addr5), "myhost") self.assertEqual(str(self.addr6), "80 default_server") + def test_to_string(self): + self.assertEqual(self.addr1.to_string(), "192.168.1.1") + self.assertEqual(self.addr2.to_string(), "192.168.1.1:* ssl") + self.assertEqual(self.addr3.to_string(), "192.168.1.1:80") + self.assertEqual(self.addr4.to_string(), "*:80 default_server ssl") + self.assertEqual(self.addr4.to_string(include_default=False), "*:80 ssl") + self.assertEqual(self.addr5.to_string(), "myhost") + self.assertEqual(self.addr6.to_string(), "80 default_server") + self.assertEqual(self.addr6.to_string(include_default=False), "80") + def test_eq(self): from certbot_nginx.obj import Addr new_addr1 = Addr.fromstring("192.168.1.1 spdy") diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index d148e89aa..d5593171a 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -48,7 +48,8 @@ class NginxParserTest(util.NginxTest): ['foo.conf', 'nginx.conf', 'server.conf', 'sites-enabled/default', 'sites-enabled/example.com', - 'sites-enabled/migration.com']]), + 'sites-enabled/migration.com', + 'sites-enabled/sslon.com']]), set(nparser.parsed.keys())) self.assertEqual([['server_name', 'somename alias another.alias']], nparser.parsed[nparser.abs_path('server.conf')]) @@ -72,7 +73,7 @@ class NginxParserTest(util.NginxTest): parsed = nparser._parse_files(nparser.abs_path( 'sites-enabled/example.com.test')) self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test')))) - self.assertEqual(3, len( + self.assertEqual(4, len( glob.glob(nparser.abs_path('sites-enabled/*.test')))) self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], @@ -136,7 +137,7 @@ class NginxParserTest(util.NginxTest): '*.www.example.com']), [], [2, 1, 0]) - self.assertEqual(7, len(vhosts)) + self.assertEqual(8, len(vhosts)) example_com = [x for x in vhosts if 'example.com' in x.filep][0] self.assertEqual(vhost3, example_com) default = [x for x in vhosts if 'default' in x.filep][0] @@ -304,8 +305,10 @@ class NginxParserTest(util.NginxTest): replace=False) c_k = nparser.get_all_certs_keys() migration_file = nparser.abs_path('sites-enabled/migration.com') + sslon_file = nparser.abs_path('sites-enabled/sslon.com') self.assertEqual(set([('foo.pem', 'bar.key', filep), - ('cert.pem', 'cert.key', migration_file) + ('cert.pem', 'cert.key', migration_file), + ('snakeoil.cert', 'snakeoil.key', sslon_file) ]), c_k) def test_parse_server_ssl(self): diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/sslon.com b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/sslon.com new file mode 100644 index 000000000..b93e6ba2d --- /dev/null +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/sslon.com @@ -0,0 +1,6 @@ +server { + server_name sslon.com; + ssl on; + ssl_certificate snakeoil.cert; + ssl_certificate_key snakeoil.key; +} diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 283e326e9..e7dacb400 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -39,6 +39,10 @@ class TlsSniPerformTest(util.NginxTest): "\xeb9\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4" ), "pending"), domain="www.example.org", account_key=account_key), + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.chall_to_challb( + challenges.TLSSNI01(token="kNdwjxOeX0I_A8DXt9Msmg"), "pending"), + domain="sslon.com", account_key=account_key), ] def setUp(self): @@ -100,7 +104,7 @@ class TlsSniPerformTest(util.NginxTest): sni_responses = self.sni.perform() - self.assertEqual(mock_setup_cert.call_count, 3) + self.assertEqual(mock_setup_cert.call_count, 4) for index, achall in enumerate(self.achalls): self.assertEqual( @@ -112,8 +116,8 @@ class TlsSniPerformTest(util.NginxTest): self.assertFalse( util.contains_at_depth(http, ['server_name', 'another.alias'], 3)) - self.assertEqual(len(sni_responses), 3) - for i in xrange(3): + self.assertEqual(len(sni_responses), 4) + for i in xrange(4): self.assertEqual(sni_responses[i], acme_responses[i]) def test_mod_config(self): @@ -123,6 +127,7 @@ class TlsSniPerformTest(util.NginxTest): v_addr1 = [obj.Addr("69.50.225.155", "9000", True, False), obj.Addr("127.0.0.1", "", False, False)] v_addr2 = [obj.Addr("myhost", "", False, True)] + v_addr2_print = [obj.Addr("myhost", "", False, False)] ll_addr = [v_addr1, v_addr2] self.sni._mod_config(ll_addr) # pylint: disable=protected-access @@ -142,7 +147,7 @@ class TlsSniPerformTest(util.NginxTest): response = self.achalls[0].response(self.account_key) else: response = self.achalls[2].response(self.account_key) - self.assertEqual(vhost.addrs, set(v_addr2)) + self.assertEqual(vhost.addrs, set(v_addr2_print)) self.assertEqual(vhost.names, set([response.z_domain])) self.assertEqual(len(vhs), 2) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index 0543000ea..dec21e791 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -47,7 +47,7 @@ class NginxTlsSni01(common.TLSSNI01): return [] addresses = [] - default_addr = "{0} default_server ssl".format( + default_addr = "{0} ssl".format( self.configurator.config.tls_sni_01_port) for achall in self.achalls: @@ -59,12 +59,10 @@ class NginxTlsSni01(common.TLSSNI01): achall.domain) return None - for addr in vhost.addrs: - if addr.default: - addresses.append([obj.Addr.fromstring(default_addr)]) - break - else: + if vhost.addrs: addresses.append(list(vhost.addrs)) + else: + addresses.append([obj.Addr.fromstring(default_addr)]) # Create challenge certs responses = [self._setup_challenge_cert(x) for x in self.achalls] @@ -141,7 +139,7 @@ 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', ' ', addr.to_string(include_default=False)] for addr in addrs] block.extend([['server_name', ' ', achall.response(achall.account_key).z_domain], @@ -155,5 +153,4 @@ class NginxTlsSni01(common.TLSSNI01): ['ssl_certificate_key', ' ', self.get_key_path(achall)], [['location', ' ', '/'], [['root', ' ', document_root]]]] + self.configurator.parser.loc["ssl_options"]) - return [['server'], block] From d54cb76432a2eff43cc9cc3c1cc4d9136eac2221 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Fri, 21 Oct 2016 15:45:57 -0700 Subject: [PATCH 228/331] Remove the curses dialog, thereby deprecating the --help and --dialog command line options (#3665) * Remove the curses dialog, thereby deprecating the --help and --dialog command line options * Deprecate --dialog and suppress --text --- .travis.yml | 1 - Vagrantfile | 1 + certbot-apache/certbot_apache/display_ops.py | 3 +- certbot/cli.py | 19 +- certbot/display/ops.py | 2 - certbot/display/util.py | 182 +----------------- certbot/interfaces.py | 3 +- certbot/log.py | 64 ------ certbot/main.py | 24 +-- certbot/plugins/manual.py | 2 +- certbot/plugins/selection.py | 6 +- certbot/plugins/util.py | 5 +- certbot/plugins/webroot.py | 5 +- certbot/plugins/webroot_test.py | 3 +- certbot/reverter.py | 4 +- certbot/tests/cli_test.py | 37 ++-- certbot/tests/display/util_test.py | 118 +----------- certbot/tests/log_test.py | 48 ----- certbot/tests/main_test.py | 9 +- letsencrypt-auto-source/letsencrypt-auto | 10 - .../pieces/bootstrappers/arch_common.sh | 1 - .../pieces/bootstrappers/deb_common.sh | 1 - .../pieces/bootstrappers/gentoo_common.sh | 1 - .../pieces/bootstrappers/mac.sh | 1 - .../pieces/bootstrappers/mageia_common.sh | 1 - .../pieces/bootstrappers/rpm_common.sh | 1 - .../pieces/bootstrappers/suse_common.sh | 1 - .../pieces/letsencrypt-auto-requirements.txt | 3 - setup.py | 6 - tests/display.py | 4 +- tox.ini | 1 - 31 files changed, 41 insertions(+), 526 deletions(-) delete mode 100644 certbot/log.py delete mode 100644 certbot/tests/log_test.py diff --git a/.travis.yml b/.travis.yml index 8101fb3a4..2c66f2a83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -103,7 +103,6 @@ addons: - python-dev - python-virtualenv - gcc - - dialog - libaugeas0 - libssl-dev - libffi-dev diff --git a/Vagrantfile b/Vagrantfile index e5975442f..23d3ddf13 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -29,6 +29,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # VM needs more memory to run test suite, got "OSError: [Errno 12] # Cannot allocate memory" when running # letsencrypt.client.tests.display.util_test.NcursesDisplayTest + # We may no longer need this. v.memory = 1024 # Handle cases when the host is behind a private network by making the diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index d7b76f83d..527de1001 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -100,5 +100,4 @@ def _vhost_menu(domain, vhosts): def _more_info_vhost(vhost): zope.component.getUtility(interfaces.IDisplay).notification( "Virtual Host Information:{0}{1}{0}{2}".format( - os.linesep, "-" * (display_util.WIDTH - 4), str(vhost)), - height=display_util.HEIGHT) + os.linesep, "-" * (display_util.WIDTH - 4), str(vhost))) diff --git a/certbot/cli.py b/certbot/cli.py index 83697d8da..8acb1f691 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -376,7 +376,7 @@ class HelpfulArgumentParser(object): # Do any post-parsing homework here - if self.verb == "renew" and not parsed_args.dialog_mode: + if self.verb == "renew": parsed_args.noninteractive_mode = True if parsed_args.staging or parsed_args.dry_run: @@ -388,17 +388,6 @@ class HelpfulArgumentParser(object): if parsed_args.must_staple: parsed_args.staple = True - # Avoid conflicting args - conficting_args = ["quiet", "noninteractive_mode", "text_mode"] - if parsed_args.dialog_mode: - for arg in conficting_args: - if getattr(parsed_args, arg): - raise errors.Error( - ("Conflicting values for displayer." - " {0} conflicts with dialog_mode").format(arg)) - elif parsed_args.verbose_count > flag_default("verbose_count"): - parsed_args.text_mode = True - if parsed_args.validate_hooks: hooks.validate_hooks(parsed_args) @@ -677,16 +666,13 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "e.g. -vvv.") helpful.add( None, "-t", "--text", dest="text_mode", action="store_true", - help="Use the text output instead of the curses UI.") + help=argparse.SUPPRESS) helpful.add( [None, "automation"], "-n", "--non-interactive", "--noninteractive", dest="noninteractive_mode", action="store_true", help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " "which ones are required if it finds one missing") - helpful.add( - None, "--dialog", dest="dialog_mode", action="store_true", - help="Run using interactive dialog menus") helpful.add( [None, "run", "certonly"], "-d", "--domains", "--domain", dest="domains", @@ -890,6 +876,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " shell constructs, so you can use this switch to disable it.") helpful.add_deprecated_argument("--agree-dev-preview", 0) + helpful.add_deprecated_argument("--dialog", 0) _create_subparsers(helpful) _paths_parser(helpful) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 662483ee0..4339cedd6 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -219,7 +219,6 @@ def success_installation(domains): _gen_https_names(domains), os.linesep, os.linesep.join(_gen_ssl_lab_urls(domains))), - height=(10 + len(domains)), pause=False) @@ -241,7 +240,6 @@ def success_renewal(domains, action): os.linesep, os.linesep.join(_gen_ssl_lab_urls(domains)), action), - height=(14 + len(domains)), pause=False) diff --git a/certbot/display/util.py b/certbot/display/util.py index c0e9386cd..30d604313 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -1,9 +1,7 @@ """Certbot display.""" -import logging import os import textwrap -import dialog import six import zope.interface @@ -12,17 +10,7 @@ from certbot import errors from certbot.display import completer -logger = logging.getLogger(__name__) - WIDTH = 72 -HEIGHT = 20 - -DSELECT_HELP = ( - "Use the arrow keys or Tab to move between window elements. Space can be " - "used to complete the input path with the selected element in the " - "directory window. Pressing enter will select the currently highlighted " - "button.") -"""Help text on how to use dialog's dselect.""" # Display exit codes OK = "ok" @@ -59,170 +47,6 @@ def _wrap_lines(msg): return os.linesep.join(fixed_l) - -def _clean(dialog_result): - """Treat sundy python-dialog return codes as CANCEL - - :param tuple dialog_result: (code, result) - :returns: the argument but with unknown codes set to -1 (Error) - :rtype: tuple - """ - code, result = dialog_result - if code in (OK, HELP): - return dialog_result - elif code in (CANCEL, ESC): - return (CANCEL, result) - else: - logger.debug("Surprising dialog return code %s", code) - return (CANCEL, result) - - -@zope.interface.implementer(interfaces.IDisplay) -class NcursesDisplay(object): - """Ncurses-based display.""" - - def __init__(self, width=WIDTH, height=HEIGHT): - super(NcursesDisplay, self).__init__() - self.dialog = dialog.Dialog(autowidgetsize=True) - assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?" - self.width = width - self.height = height - - def notification(self, message, height=10, pause=False): - # pylint: disable=unused-argument - """Display a notification to the user and wait for user acceptance. - - .. todo:: It probably makes sense to use one of the transient message - types for pause. It isn't straightforward how best to approach - the matter though given the context of our messages. - http://pythondialog.sourceforge.net/doc/widgets.html#displaying-transient-messages - - :param str message: Message to display - :param int height: Height of the dialog box - :param bool pause: Not applicable to NcursesDisplay - - """ - self.dialog.msgbox(message) - - def menu(self, message, choices, ok_label="OK", cancel_label="Cancel", - help_label="", **unused_kwargs): - """Display a menu. - - :param str message: title of menu - - :param choices: menu lines, len must be > 0 - :type choices: list of tuples (`tag`, `item`) tags must be unique or - list of items (tags will be enumerated) - - :param str ok_label: label of the OK button - :param str help_label: label of the help button - :param dict unused_kwargs: absorbs default / cli_args - - :returns: tuple of the form (`code`, `index`) where - `code` - display exit code - `int` - index of the selected item - :rtype: tuple - - """ - menu_options = { - "choices": choices, - "ok_label": ok_label, - "cancel_label": cancel_label, - "help_button": bool(help_label), - "help_label": help_label, - "width": self.width, - "height": self.height, - "menu_height": self.height - 6, - } - - # Can accept either tuples or just the actual choices - if choices and isinstance(choices[0], tuple): - # pylint: disable=star-args - code, selection = _clean(self.dialog.menu(message, **menu_options)) - - # Return the selection index - for i, choice in enumerate(choices): - if choice[0] == selection: - return code, i - - return code, -1 - - else: - # "choices" is not formatted the way the dialog.menu expects... - menu_options["choices"] = [ - (str(i), choice) for i, choice in enumerate(choices, 1) - ] - # pylint: disable=star-args - code, index = _clean(self.dialog.menu(message, **menu_options)) - - if code == CANCEL or index == "": - return code, -1 - - return code, int(index) - 1 - - def input(self, message, **unused_kwargs): - """Display an input box to the user. - - :param str message: Message to display that asks for input. - :param dict _kwargs: absorbs default / cli_args - - :returns: tuple of the form (`code`, `string`) where - `code` - display exit code - `string` - input entered by the user - - """ - return self.dialog.inputbox(message) - - def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): - """Display a Yes/No dialog box. - - Yes and No label must begin with different letters. - - :param str message: message to display to user - :param str yes_label: label on the "yes" button - :param str no_label: label on the "no" button - :param dict _kwargs: absorbs default / cli_args - - :returns: if yes_label was selected - :rtype: bool - - """ - return self.dialog.DIALOG_OK == self.dialog.yesno( - message, yes_label=yes_label, no_label=no_label) - - def checklist(self, message, tags, default_status=True, **unused_kwargs): - """Displays a checklist. - - :param message: Message to display before choices - :param list tags: where each is of type :class:`str` len(tags) > 0 - :param bool default_status: If True, items are in a selected state by - default. - :param dict _kwargs: absorbs default / cli_args - - - :returns: tuple of the form (`code`, `list_tags`) where - `code` - display exit code - `list_tags` - list of str tags selected by the user - - """ - choices = [(tag, "", default_status) for tag in tags] - return self.dialog.checklist(message, choices=choices) - - def directory_select(self, message, **unused_kwargs): - """Display a directory selection screen. - - :param str message: prompt to give the user - - :returns: tuple of the form (`code`, `string`) where - `code` - display exit code - `string` - input entered by the user - - """ - root_directory = os.path.abspath(os.sep) - return self.dialog.dselect( - filepath=root_directory, help_button=True, title=message) - - @zope.interface.implementer(interfaces.IDisplay) class FileDisplay(object): """File-based display.""" @@ -231,12 +55,11 @@ class FileDisplay(object): super(FileDisplay, self).__init__() self.outfile = outfile - def notification(self, message, height=10, pause=True): + def notification(self, message, pause=True): # pylint: disable=unused-argument """Displays a notification and waits for user acceptance. :param str message: Message to display - :param int height: No effect for FileDisplay :param bool pause: Whether or not the program should pause for the user's confirmation @@ -496,12 +319,11 @@ class NoninteractiveDisplay(object): msg += "\n\n(You can set this with the {0} flag)".format(cli_flag) raise errors.MissingCommandlineFlag(msg) - def notification(self, message, height=10, pause=False): + def notification(self, message, pause=False): # pylint: disable=unused-argument """Displays a notification without waiting for user acceptance. :param str message: Message to display to stdout - :param int height: No effect for NoninteractiveDisplay :param bool pause: The NoninteractiveDisplay waits for no keyboard """ diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 42a952f10..5872c38d8 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -378,11 +378,10 @@ class IInstaller(IPlugin): class IDisplay(zope.interface.Interface): """Generic display.""" - def notification(message, height, pause): + def notification(message, pause): """Displays a string message :param str message: Message to display - :param int height: Height of dialog box if applicable :param bool pause: Whether or not the application should pause for confirmation (if available) diff --git a/certbot/log.py b/certbot/log.py deleted file mode 100644 index 62241254a..000000000 --- a/certbot/log.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Logging utilities.""" -import logging - -import dialog - -from certbot.display import util as display_util - - -class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods - """Logging handler using dialog info box. - - :ivar int height: Height of the info box (without padding). - :ivar int width: Width of the info box (without padding). - :ivar list lines: Lines to be displayed in the info box. - :ivar d: Instance of :class:`dialog.Dialog`. - - """ - - PADDING_HEIGHT = 2 - PADDING_WIDTH = 4 - - def __init__(self, level=logging.NOTSET, height=display_util.HEIGHT, - width=display_util.WIDTH - 4, d=None): - # Handler not new-style -> no super - logging.Handler.__init__(self, level) - self.height = height - self.width = width - # "dialog" collides with module name... - self.d = dialog.Dialog() if d is None else d - self.lines = [] - - def emit(self, record): - """Emit message to a dialog info box. - - Only show the last (self.height) lines; note that lines can wrap - at self.width, so a single line could actually be multiple - lines. - - """ - for line in self.format(record).splitlines(): - # check for lines that would wrap - cur_out = line - while len(cur_out) > self.width: - # find first space before self.width chars into cur_out - last_space_pos = cur_out.rfind(' ', 0, self.width) - - if last_space_pos == -1: - # no spaces, just cut them off at whatever - self.lines.append(cur_out[0:self.width]) - cur_out = cur_out[self.width:] - else: - # cut off at last space - self.lines.append(cur_out[0:last_space_pos]) - cur_out = cur_out[last_space_pos + 1:] - if cur_out != '': - self.lines.append(cur_out) - - # show last 16 lines - content = '\n'.join(self.lines[-self.height:]) - - # add the padding around the box - self.d.infobox( - content, self.height + self.PADDING_HEIGHT, - self.width + self.PADDING_WIDTH) diff --git a/certbot/main.py b/certbot/main.py index 5c8105ddd..aa9b1892f 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1,7 +1,6 @@ """Certbot main entry point.""" from __future__ import print_function import atexit -import dialog import functools import logging.handlers import os @@ -27,7 +26,6 @@ from certbot import errors from certbot import hooks from certbot import interfaces from certbot import util -from certbot import log from certbot import reporter from certbot import renewal from certbot import storage @@ -614,14 +612,9 @@ def setup_log_file_handler(config, logfile, fmt): return handler, log_file_path -def _cli_log_handler(config, level, fmt): - if config.text_mode or config.noninteractive_mode or config.verb == "renew": - handler = colored_logging.StreamHandler() - handler.setFormatter(logging.Formatter(fmt)) - else: - handler = log.DialogHandler() - # dialog box is small, display as less as possible - handler.setFormatter(logging.Formatter("%(message)s")) +def _cli_log_handler(level, fmt): + handler = colored_logging.StreamHandler() + handler.setFormatter(logging.Formatter(fmt)) handler.setLevel(level) return handler @@ -641,7 +634,7 @@ def setup_logging(config): level = -config.verbose_count * 10 file_handler, log_file_path = setup_log_file_handler( config, logfile=logfile, fmt=file_fmt) - cli_handler = _cli_log_handler(config, level, cli_fmt) + cli_handler = _cli_log_handler(level, cli_fmt) # TODO: use fileConfig? @@ -687,10 +680,7 @@ def _handle_exception(exc_type, exc_value, trace, config): # Here we're passing a client or ACME error out to the client at the shell # Tell the user a bit about what happened, without overwhelming # them with a full traceback - if issubclass(exc_type, dialog.error): - err = exc_value.complete_message() - else: - err = traceback.format_exception_only(exc_type, exc_value)[0] + err = traceback.format_exception_only(exc_type, exc_value)[0] # Typical error from the ACME module: # acme.messages.Error: urn:ietf:params:acme:error:malformed :: The # request message was malformed :: Error creating new registration @@ -764,10 +754,8 @@ def main(cli_args=sys.argv[1:]): displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w")) elif config.noninteractive_mode: displayer = display_util.NoninteractiveDisplay(sys.stdout) - elif config.text_mode: - displayer = display_util.FileDisplay(sys.stdout) else: - displayer = display_util.NcursesDisplay() + displayer = display_util.FileDisplay(sys.stdout) zope.component.provideUtility(displayer) # Reporter diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 2ef49d7f4..48aec6e35 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -243,7 +243,7 @@ s.serve_forever()" """ # pylint: disable=no-self-use # TODO: IDisplay wraps messages, breaking the command #answer = zope.component.getUtility(interfaces.IDisplay).notification( - # message=message, height=25, pause=True) + # message=message, pause=True) sys.stdout.write(message) six.moves.input("Press ENTER to continue") diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 3fbc510ba..8f371f586 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -119,8 +119,7 @@ def choose_plugin(prepared, question): z_util(interfaces.IDisplay).notification( "The selected plugin encountered an error while parsing " "your server configuration and cannot be used. The error " - "was:\n\n{0}".format(plugin_ep.prepare()), - height=display_util.HEIGHT, pause=False) + "was:\n\n{0}".format(plugin_ep.prepare()), pause=False) else: return plugin_ep elif code == display_util.HELP: @@ -128,8 +127,7 @@ def choose_plugin(prepared, question): msg = "Reported Error: %s" % prepared[index].prepare() else: msg = prepared[index].init().more_info() - z_util(interfaces.IDisplay).notification( - msg, height=display_util.HEIGHT) + z_util(interfaces.IDisplay).notification(msg) else: return None diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index 786f6ca92..8f6a62a7f 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -103,7 +103,7 @@ def already_listening_socket(port, renewer=False): "Port {0} is already in use by another process. This will " "prevent us from binding to that port. Please stop the " "process that is populating the port in question and try " - "again. {1}".format(port, extra), height=13) + "again. {1}".format(port, extra)) return True finally: testsocket.close() @@ -151,8 +151,7 @@ def already_listening_psutil(port, renewer=False): "The program {0} (process ID {1}) is already listening " "on TCP port {2}. This will prevent us from binding to " "that port. Please stop the {0} program temporarily " - "and then try again.{3}".format(name, pid, port, extra), - height=13) + "and then try again.{3}".format(name, pid, port, extra)) return True except (psutil.NoSuchProcess, psutil.AccessDenied): # Perhaps the result of a race where the process could have diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index 1cd1d879a..2c449fdca 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -140,9 +140,8 @@ to serve all files under specified web root ({0}).""" code, webroot = display.directory_select( "Input the webroot for {0}:".format(domain)) if code == display_util.HELP: - # Help can currently only be selected - # when using the ncurses interface - display.notification(display_util.DSELECT_HELP) + # Displaying help is not currently implemented + return None elif code == display_util.CANCEL: return None else: # code == display_util.OK diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py index 5d784a75c..2aa7e8acc 100644 --- a/certbot/plugins/webroot_test.py +++ b/certbot/plugins/webroot_test.py @@ -111,8 +111,7 @@ class AuthenticatorTest(unittest.TestCase): self.assertTrue(mock_display.notification.called) for call in mock_display.notification.call_args_list: - self.assertTrue(imaginary_dir in call[0][0] or - display_util.DSELECT_HELP == call[0][0]) + self.assertTrue(imaginary_dir in call[0][0]) self.assertTrue(mock_display.directory_select.called) for call in mock_display.directory_select.call_args_list: diff --git a/certbot/reverter.py b/certbot/reverter.py index 098c74911..714a38b8b 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -15,8 +15,6 @@ from certbot import errors from certbot import interfaces from certbot import util -from certbot.display import util as display_util - logger = logging.getLogger(__name__) @@ -183,7 +181,7 @@ class Reverter(object): if for_logging: return os.linesep.join(output) zope.component.getUtility(interfaces.IDisplay).notification( - os.linesep.join(output), display_util.HEIGHT) + os.linesep.join(output)) def add_to_temp_checkpoint(self, save_files, save_notes): """Add files to temporary checkpoint. diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 5d2051cfe..ce01d69ff 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -5,7 +5,6 @@ from __future__ import print_function import argparse -import dialog import functools import itertools import os @@ -51,6 +50,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.config_dir = os.path.join(self.tmp_dir, 'config') self.work_dir = os.path.join(self.tmp_dir, 'work') self.logs_dir = os.path.join(self.tmp_dir, 'logs') + os.mkdir(self.logs_dir) self.standard_args = ['--config-dir', self.config_dir, '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, '--text'] @@ -98,6 +98,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue("--configurator" in out) self.assertTrue("how a cert is deployed" in out) self.assertTrue("--manual-test-mode" in out) + self.assertTrue("--text" not in out) + self.assertTrue("--dialog" not in out) out = self._help_output(['-h', 'nginx']) if "nginx" in plugins: @@ -163,12 +165,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._cli_missing_flag(args, "--agree-tos") @mock.patch('certbot.main.renew') - def test_gui(self, renew): + def test_no_gui(self, renew): args = ['renew', '--dialog'] - # --text conflicts with --dialog - self.standard_args.remove('--text') + # --dialog should have no effect self._call(args) - self.assertFalse(renew.call_args[0][0].noninteractive_mode) + self.assertTrue(renew.call_args[0][0].noninteractive_mode) @mock.patch('certbot.main.client.acme_client.Client') @mock.patch('certbot.main._determine_account') @@ -656,9 +657,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods log_out="not yet due", should_renew=False) def _dump_log(self): - with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: - print("Logs:") - print(lf.read()) + print("Logs:") + log_path = os.path.join(self.logs_dir, "letsencrypt.log") + if os.path.exists(log_path): + with open(log_path) as lf: + print(lf.read()) def _make_lineage(self, testfile): """Creates a lineage defined by testfile. @@ -977,13 +980,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_sys.exit.assert_called_with(''.join( traceback.format_exception_only(KeyboardInterrupt, interrupt))) - # Test dialog errors - exception = dialog.error(message="test message") - main._handle_exception( - dialog.DialogError, exc_value=exception, trace=None, config=None) - error_msg = mock_sys.exit.call_args_list[-1][0][0] - self.assertTrue("test message" in error_msg) - def test_read_file(self): rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo')) self.assertRaises( @@ -1070,17 +1066,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue( email in mock_utility().add_message.call_args[0][0]) - def test_conflicting_args(self): - args = ['renew', '--dialog', '--text'] - self.assertRaises(errors.Error, self._call, args) - - def test_text_mode_when_verbose(self): - parse = self._get_argument_parser() - short_args = ['-v'] - namespace = parse(short_args) - self.assertTrue(namespace.text_mode) - - class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index c1d33eff4..b04235bd9 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -13,122 +13,6 @@ CHOICES = [("First", "Description1"), ("Second", "Description2")] TAGS = ["tag1", "tag2", "tag3"] TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")] - -class NcursesDisplayTest(unittest.TestCase): - """Test ncurses display. - - Since this is mostly a wrapper, it might be more helpful to test the - actual dialog boxes. The test file located in ./tests/display.py - (relative to the root of the repository) will actually display the - various boxes but requires the user to do the verification. If - something seems amiss please use that test script to debug it, the - automatic tests rely on too much mocking. - - """ - def setUp(self): - super(NcursesDisplayTest, self).setUp() - self.displayer = display_util.NcursesDisplay() - - self.default_menu_options = { - "choices": CHOICES, - "ok_label": "OK", - "cancel_label": "Cancel", - "help_button": False, - "help_label": "", - "width": display_util.WIDTH, - "height": display_util.HEIGHT, - "menu_height": display_util.HEIGHT - 6, - } - - @mock.patch("certbot.display.util.dialog.Dialog.msgbox") - def test_notification(self, mock_msgbox): - """Kind of worthless... one liner.""" - self.displayer.notification("message") - self.assertEqual(mock_msgbox.call_count, 1) - - @mock.patch("certbot.display.util.dialog.Dialog.menu") - def test_menu_tag_and_desc(self, mock_menu): - mock_menu.return_value = (display_util.OK, "First") - - ret = self.displayer.menu("Message", CHOICES) - mock_menu.assert_called_with("Message", **self.default_menu_options) - - self.assertEqual(ret, (display_util.OK, 0)) - - @mock.patch("certbot.display.util.dialog.Dialog.menu") - def test_menu_tag_and_desc_cancel(self, mock_menu): - mock_menu.return_value = (display_util.CANCEL, "") - - ret = self.displayer.menu("Message", CHOICES) - - mock_menu.assert_called_with("Message", **self.default_menu_options) - - self.assertEqual(ret, (display_util.CANCEL, -1)) - - @mock.patch("certbot.display.util.dialog.Dialog.menu") - def test_menu_desc_only(self, mock_menu): - mock_menu.return_value = (display_util.OK, "1") - - ret = self.displayer.menu("Message", TAGS, help_label="More Info") - - self.default_menu_options.update( - choices=TAGS_CHOICES, help_button=True, help_label="More Info") - mock_menu.assert_called_with("Message", **self.default_menu_options) - - self.assertEqual(ret, (display_util.OK, 0)) - - @mock.patch("certbot.display.util.dialog.Dialog.menu") - def test_menu_desc_only_help(self, mock_menu): - mock_menu.return_value = (display_util.HELP, "2") - - ret = self.displayer.menu("Message", TAGS, help_label="More Info") - - self.assertEqual(ret, (display_util.HELP, 1)) - - @mock.patch("certbot.display.util.dialog.Dialog.menu") - def test_menu_desc_only_cancel(self, mock_menu): - mock_menu.return_value = (display_util.CANCEL, "") - - ret = self.displayer.menu("Message", TAGS, help_label="More Info") - - self.assertEqual(ret, (display_util.CANCEL, -1)) - - @mock.patch("certbot.display.util." - "dialog.Dialog.inputbox") - def test_input(self, mock_input): - mock_input.return_value = (mock.MagicMock(), mock.MagicMock()) - self.displayer.input("message") - self.assertEqual(mock_input.call_count, 1) - - @mock.patch("certbot.display.util.dialog.Dialog.yesno") - def test_yesno(self, mock_yesno): - mock_yesno.return_value = display_util.OK - - self.assertTrue(self.displayer.yesno("message")) - - mock_yesno.assert_called_with( - "message", yes_label="Yes", no_label="No") - - @mock.patch("certbot.display.util." - "dialog.Dialog.checklist") - def test_checklist(self, mock_checklist): - mock_checklist.return_value = (mock.MagicMock(), mock.MagicMock()) - self.displayer.checklist("message", TAGS) - - choices = [ - (TAGS[0], "", True), - (TAGS[1], "", True), - (TAGS[2], "", True), - ] - mock_checklist.assert_called_with("message", choices=choices) - - @mock.patch("certbot.display.util.dialog.Dialog.dselect") - def test_directory_select(self, mock_dselect): - mock_dselect.return_value = (mock.MagicMock(), mock.MagicMock()) - self.displayer.directory_select("message") - self.assertEqual(mock_dselect.call_count, 1) - - class FileOutputDisplayTest(unittest.TestCase): """Test stdout display. @@ -142,7 +26,7 @@ class FileOutputDisplayTest(unittest.TestCase): self.displayer = display_util.FileDisplay(self.mock_stdout) def test_notification_no_pause(self): - self.displayer.notification("message", 10, False) + self.displayer.notification("message", False) string = self.mock_stdout.write.call_args[0][0] self.assertTrue("message" in string) diff --git a/certbot/tests/log_test.py b/certbot/tests/log_test.py deleted file mode 100644 index a4f394870..000000000 --- a/certbot/tests/log_test.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Tests for certbot.log.""" -import logging -import unittest - -import mock - - -class DialogHandlerTest(unittest.TestCase): - - def setUp(self): - self.d = mock.MagicMock() - - from certbot.log import DialogHandler - self.handler = DialogHandler(height=2, width=6, d=self.d) - self.handler.PADDING_HEIGHT = 2 - self.handler.PADDING_WIDTH = 4 - - def test_adds_padding(self): - self.handler.emit(logging.makeLogRecord({})) - self.d.infobox.assert_called_once_with(mock.ANY, 4, 10) - - def test_args_in_msg_get_replaced(self): - assert len('123456') <= self.handler.width - self.handler.emit(logging.makeLogRecord( - {'msg': '123%s', 'args': (456,)})) - self.d.infobox.assert_called_once_with('123456', mock.ANY, mock.ANY) - - def test_wraps_nospace_is_greedy(self): - assert len('1234567') > self.handler.width - self.handler.emit(logging.makeLogRecord({'msg': '1234567'})) - self.d.infobox.assert_called_once_with('123456\n7', mock.ANY, mock.ANY) - - def test_wraps_at_whitespace(self): - assert len('123 567') > self.handler.width - self.handler.emit(logging.makeLogRecord({'msg': '123 567'})) - self.d.infobox.assert_called_once_with('123\n567', mock.ANY, mock.ANY) - - def test_only_last_lines_are_printed(self): - assert len('a\nb\nc'.split()) > self.handler.height - self.handler.emit(logging.makeLogRecord({'msg': 'a\n\nb\nc'})) - self.d.infobox.assert_called_once_with('b\nc', mock.ANY, mock.ANY) - - def test_non_str(self): - self.handler.emit(logging.makeLogRecord({'msg': {'foo': 'bar'}})) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index f7a6c5896..045a593e9 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -11,7 +11,6 @@ from certbot import colored_logging from certbot import constants from certbot import configuration from certbot import errors -from certbot import log from certbot.plugins import disco as plugins_disco class MainTest(unittest.TestCase): @@ -55,9 +54,9 @@ class ObtainCertTest(unittest.TestCase): mock_notification = self.mock_get_utility().notification mock_notification.side_effect = self._assert_no_pause mock_auth.return_value = ('reinstall', mock.ANY) - self._call('certonly --webroot -d example.com -t'.split()) + self._call('certonly --webroot -d example.com'.split()) - def _assert_no_pause(self, message, height=42, pause=True): + def _assert_no_pause(self, message, pause=True): # pylint: disable=unused-argument self.assertFalse(pause) @@ -89,7 +88,7 @@ class SetupLoggingTest(unittest.TestCase): def setUp(self): self.config = mock.Mock( logs_dir=tempfile.mkdtemp(), - noninteractive_mode=False, quiet=False, text_mode=False, + noninteractive_mode=False, quiet=False, verbose_count=constants.CLI_DEFAULTS['verbose_count']) def tearDown(self): @@ -107,7 +106,7 @@ class SetupLoggingTest(unittest.TestCase): cli_handler = mock_get_logger().addHandler.call_args_list[0][0][0] self.assertEqual(cli_handler.level, -self.config.verbose_count * 10) self.assertTrue( - isinstance(cli_handler, log.DialogHandler)) + isinstance(cli_handler, colored_logging.StreamHandler)) @mock.patch('certbot.main.logging.getLogger') def test_quiet_mode(self, mock_get_logger): diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index b5adeea86..4ec226d39 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -248,7 +248,6 @@ BootstrapDebCommon() { python-dev \ $virtualenv \ gcc \ - dialog \ $augeas_pkg \ libssl-dev \ libffi-dev \ @@ -307,7 +306,6 @@ BootstrapRpmCommon() { pkgs=" gcc - dialog augeas-libs openssl openssl-devel @@ -361,7 +359,6 @@ BootstrapSuseCommon() { python-devel \ python-virtualenv \ gcc \ - dialog \ augeas-lenses \ libopenssl-devel \ libffi-devel \ @@ -380,7 +377,6 @@ BootstrapArchCommon() { python2 python-virtualenv gcc - dialog augeas openssl libffi @@ -404,7 +400,6 @@ BootstrapGentooCommon() { PACKAGES=" dev-lang/python:2.7 dev-python/virtualenv - dev-util/dialog app-admin/augeas dev-libs/openssl dev-libs/libffi @@ -449,7 +444,6 @@ BootstrapMac() { fi $pkgcmd augeas - $pkgcmd dialog if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. @@ -496,7 +490,6 @@ BootstrapMageiaCommon() { if ! $SUDO urpmi --force \ git \ gcc \ - cdialog \ python-augeas \ libopenssl-devel \ libffi-devel \ @@ -701,9 +694,6 @@ pyRFC3339==1.0 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 python-augeas==0.5.0 \ --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 -python2-pythondialog==3.3.0 \ - --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \ - --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa pytz==2015.7 \ --hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \ --hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \ diff --git a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh index 39e2da5fe..333f56ff7 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh @@ -10,7 +10,6 @@ BootstrapArchCommon() { python2 python-virtualenv gcc - dialog augeas openssl libffi diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index 8eb7e16ee..0188cadc5 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -94,7 +94,6 @@ BootstrapDebCommon() { python-dev \ $virtualenv \ gcc \ - dialog \ $augeas_pkg \ libssl-dev \ libffi-dev \ diff --git a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh index 580b69a0d..4b1e3b545 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh @@ -2,7 +2,6 @@ BootstrapGentooCommon() { PACKAGES=" dev-lang/python:2.7 dev-python/virtualenv - dev-util/dialog app-admin/augeas dev-libs/openssl dev-libs/libffi diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh index 2b04977c8..a28b410d2 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh @@ -15,7 +15,6 @@ BootstrapMac() { fi $pkgcmd augeas - $pkgcmd dialog if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh index d6651574a..63649a974 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh @@ -11,7 +11,6 @@ BootstrapMageiaCommon() { if ! $SUDO urpmi --force \ git \ gcc \ - cdialog \ python-augeas \ libopenssl-devel \ libffi-devel \ diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 2fd629ff8..26d717ea1 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -43,7 +43,6 @@ BootstrapRpmCommon() { pkgs=" gcc - dialog augeas-libs openssl openssl-devel diff --git a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh index 9ac295922..bd4d9c68d 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh @@ -11,7 +11,6 @@ BootstrapSuseCommon() { python-devel \ python-virtualenv \ gcc \ - dialog \ augeas-lenses \ libopenssl-devel \ libffi-devel \ diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 1803d51b8..de83178b0 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -110,9 +110,6 @@ pyRFC3339==1.0 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 python-augeas==0.5.0 \ --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 -python2-pythondialog==3.3.0 \ - --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \ - --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa pytz==2015.7 \ --hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \ --hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \ diff --git a/setup.py b/setup.py index 6d0909ea8..f2d021c97 100644 --- a/setup.py +++ b/setup.py @@ -51,12 +51,6 @@ install_requires = [ 'zope.interface', ] -# Debian squeeze support, cf. #280 -if sys.version_info[0] == 2: - install_requires.append('python2-pythondialog>=3.2.2rc1') -else: - install_requires.append('pythondialog>=3.2.2rc1') - # env markers in extras_require cause problems with older pip: #517 # Keep in sync with conditional_requirements.py. if sys.version_info < (2, 7): diff --git a/tests/display.py b/tests/display.py index ecb7c279b..7400788a3 100644 --- a/tests/display.py +++ b/tests/display.py @@ -18,5 +18,5 @@ def test_visual(displayer, choices): if __name__ == "__main__": - for displayer in util.NcursesDisplay(), util.FileDisplay(sys.stdout): - test_visual(displayer, util_test.CHOICES) + displayer = util.FileDisplay(sys.stdout): + test_visual(displayer, util_test.CHOICES) diff --git a/tox.ini b/tox.ini index ac39e995e..162601205 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,6 @@ deps = py{26,27}-oldest: dnspython>=1.12 py{26,27}-oldest: psutil==2.1.0 py{26,27}-oldest: PyOpenSSL==0.13 - py{26,27}-oldest: python2-pythondialog==3.2.2rc1 [testenv:py33] commands = From 98911d0c3c4a9e3db770293f61395e8976363690 Mon Sep 17 00:00:00 2001 From: Jay Faulkner Date: Mon, 24 Oct 2016 14:51:50 -0700 Subject: [PATCH 229/331] Fix issue with suggest_unsafe undeclared (#3685) Added missing declaration of support_unsafe and unit test to prevent regression. Issue #3672 --- certbot/display/ops.py | 1 + certbot/tests/display/ops_test.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 4339cedd6..6762c2244 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -37,6 +37,7 @@ def get_email(invalid=False, optional=True): if optional: if invalid: msg += unsafe_suggestion + suggest_unsafe = False else: suggest_unsafe = True else: diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index bc0696f9c..82ed325c8 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -72,6 +72,14 @@ class GetEmailTest(unittest.TestCase): self.assertTrue( "--register-unsafely-without-email" not in call[0][0]) + def test_optional_invalid_unsafe(self): + invalid_txt = "There seem to be problems" + self.input.return_value = (display_util.OK, "foo@bar.baz") + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: + mock_safe_email.side_effect = [False, True] + self._call(invalid=True) + self.assertTrue(invalid_txt in self.input.call_args[0][0]) + class ChooseAccountTest(unittest.TestCase): """Tests for certbot.display.ops.choose_account.""" From 1dafaec5a94d58dce46bd4efaf0f12d028a1674d Mon Sep 17 00:00:00 2001 From: Patrick Figel Date: Tue, 25 Oct 2016 22:56:38 +0200 Subject: [PATCH 230/331] Update CLI usage docs for --csr (#3677) With #2403 and #3046, certbot gained the ability to parse CSRs encoded as PEM and without a SAN extension. Update the CLI usage docs to reflect this change. --- certbot/cli.py | 6 ++---- docs/cli-help.txt | 8 +++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 8acb1f691..f840300fc 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -901,10 +901,8 @@ def _create_subparsers(helpful): 'Encrypt server, set this to "".') helpful.add("certonly", "--csr", type=read_file, - help="Path to a Certificate Signing Request (CSR) in DER" - " format; note that the .csr file *must* contain a Subject" - " Alternative Name field for each domain you want certified." - " Currently --csr only works with the 'certonly' subcommand'") + help="Path to a Certificate Signing Request (CSR) in DER or PEM format." + " Currently --csr only works with the 'certonly' subcommand.") helpful.add("rollback", "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), diff --git a/docs/cli-help.txt b/docs/cli-help.txt index f7340c48b..5ff781aa3 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -229,11 +229,9 @@ certonly: the port Certbot listens on. A conforming ACME server will still attempt to connect on port 80. (default: 80) - --csr CSR Path to a Certificate Signing Request (CSR) in DER - format; note that the .csr file *must* contain a - Subject Alternative Name field for each domain you - want certified. Currently --csr only works with the - 'certonly' subcommand' (default: None) + --csr CSR Path to a Certificate Signing Request (CSR) in DER or + PEM format. Currently --csr only works with the + 'certonly' subcommand. (default: None) install: Options for modifying how a cert is deployed From 6ad08f4f646a6dc4488f3a812947de39d5f3aee7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 25 Oct 2016 18:51:01 -0700 Subject: [PATCH 231/331] Fix link to Docker's user guide (#3651) * Fix link to Docker's user guide * Update link to the Docker installation guide --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 57589349b..676364117 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -119,7 +119,7 @@ For more information about the layout of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. .. _Docker: https://docker.com -.. _`install Docker`: https://docs.docker.com/userguide/ +.. _`install Docker`: https://docs.docker.com/engine/installation/ Operating System Packages From 42180ee9b5a9d924b86455caa3ba23e2871d0f43 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 26 Oct 2016 14:34:01 -0700 Subject: [PATCH 232/331] fix travis tests? (#3695) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2c66f2a83..373e16174 100644 --- a/.travis.yml +++ b/.travis.yml @@ -114,7 +114,6 @@ addons: - apache2 - libapache2-mod-wsgi - libapache2-mod-macro - - sudo install: "travis_retry pip install tox coveralls" script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)' From 4b5db7aec4d75b79a2cfa65f2d435d7c181a0ea1 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Wed, 26 Oct 2016 15:43:40 -0700 Subject: [PATCH 233/331] Allow user to select all domains by typing empty string at checklist (#3693) * Allow user to select all domains by typing empty string at checklist --- certbot/display/util.py | 5 ++++- certbot/tests/display/util_test.py | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 30d604313..0ad8fa200 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -179,9 +179,12 @@ class FileDisplay(object): self._print_menu(message, tags) code, ans = self.input("Select the appropriate numbers separated " - "by commas and/or spaces") + "by commas and/or spaces, or leave input " + "blank to select all options shown") if code == OK: + if len(ans.strip()) == 0: + ans = " ".join(str(x) for x in range(1, len(tags)+1)) indices = separate_list_input(ans) selected_tags = self._scrub_checklist_input(indices, tags) if selected_tags: diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index b04235bd9..fa1cb89ba 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -79,6 +79,13 @@ class FileOutputDisplayTest(unittest.TestCase): self.assertEqual( (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2"]))) + @mock.patch("certbot.display.util.FileDisplay.input") + def test_checklist_empty(self, mock_input): + mock_input.return_value = (display_util.OK, "") + code, tag_list = self.displayer.checklist("msg", TAGS) + self.assertEqual( + (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2", "tag3"]))) + @mock.patch("certbot.display.util.FileDisplay.input") def test_checklist_miss_valid(self, mock_input): mock_input.side_effect = [ From 88076e46c71fe99c7d3e121bb0fc73aa6fbc80fa Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 26 Oct 2016 18:07:33 -0700 Subject: [PATCH 234/331] Improve debug logs. (#3126) Print request and response bodies with newlines, rather than all on one line. Remove "Omitted empty field" log, which gets logged meaninglessly for every JSON serialization. Remove duplicated logging of responses. Log the base64 version of the nonce, rather than turning it into bytes and logging the backslash-escaped version of those bytes. Only pass -vv in tests. --- acme/acme/client.py | 40 +++++++++++++++++++++++------------- acme/acme/client_test.py | 23 +++++++++++++++++++++ acme/acme/jose/json_util.py | 4 ---- tests/integration/_common.sh | 2 +- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 6a648bb92..d041cc7d7 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -1,4 +1,5 @@ """ACME client API.""" +import base64 import collections import datetime from email.utils import parsedate_tz @@ -28,6 +29,8 @@ logger = logging.getLogger(__name__) if sys.version_info < (2, 7, 9): # pragma: no cover requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() +DER_CONTENT_TYPE = 'application/pkix-cert' + class Client(object): # pylint: disable=too-many-instance-attributes """ACME client. @@ -45,7 +48,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes `verify_ssl`. """ - DER_CONTENT_TYPE = 'application/pkix-cert' def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True, net=None): @@ -304,7 +306,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes # TODO: assert len(authzrs) == number of SANs req = messages.CertificateRequest(csr=csr) - content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument + content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self.net.post( authzrs[0].new_cert_uri, # TODO: acme-spec #90 req, @@ -406,7 +408,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes :rtype: tuple """ - content_type = self.DER_CONTENT_TYPE # TODO: make it a param + content_type = DER_CONTENT_TYPE # TODO: make it a param response = self.net.get(uri, headers={'Accept': content_type}, content_type=content_type) return response, jose.ComparableX509(OpenSSL.crypto.load_certificate( @@ -521,10 +523,11 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes :rtype: `.JWS` """ - jobj = obj.json_dumps().encode() - logger.debug('Serialized JSON: %s', jobj) + jobj = obj.json_dumps(indent=2).encode() + logger.debug('JWS payload:\n%s', jobj) return jws.JWS.sign( - payload=jobj, key=self.key, alg=self.alg, nonce=nonce).json_dumps() + payload=jobj, key=self.key, alg=self.alg, + nonce=nonce).json_dumps(indent=2) @classmethod def _check_response(cls, response, content_type=None): @@ -545,9 +548,6 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes :raises .ClientError: In case of other networking errors. """ - logger.debug('Received response %s (headers: %s): %r', - response, response.headers, response.content) - response_ct = response.headers.get('Content-Type') try: # TODO: response.json() is called twice, once here, and @@ -599,14 +599,26 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes """ - logging.debug('Sending %s request to %s. args: %r, kwargs: %r', - method, url, args, kwargs) + if method == "POST": + logging.debug('Sending POST request to %s:\n%s', + url, kwargs['data']) + else: + logging.debug('Sending %s request to %s.', method, url) kwargs['verify'] = self.verify_ssl kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('User-Agent', self.user_agent) response = self.session.request(method, url, *args, **kwargs) - logging.debug('Received %s. Headers: %s. Content: %r', - response, response.headers, response.content) + # If content is DER, log the base64 of it instead of raw bytes, to keep + # binary data out of the logs. + if response.headers.get("Content-Type") == DER_CONTENT_TYPE: + debug_content = base64.b64encode(response.content) + else: + debug_content = response.content + logger.debug('Received response:\nHTTP %d\n%s\n\n%s', + response.status_code, + "\n".join(["{0}: {1}".format(k, v) + for k, v in response.headers.items()]), + debug_content) return response def head(self, *args, **kwargs): @@ -631,7 +643,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes decoded_nonce = jws.Header._fields['nonce'].decode(nonce) except jose.DeserializationError as error: raise errors.BadNonce(nonce, error) - logger.debug('Storing nonce: %r', decoded_nonce) + logger.debug('Storing nonce: %s', nonce) self._nonces.add(decoded_nonce) else: raise errors.MissingNonce(response) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 374f8954c..e0403ef28 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -534,6 +534,29 @@ class ClientNetworkTest(unittest.TestCase): 'HEAD', 'http://example.com/', 'foo', headers=mock.ANY, verify=mock.ANY, bar='baz') + @mock.patch('acme.client.logger') + def test_send_request_get_der(self, mock_logger): + self.net.session = mock.MagicMock() + self.net.session.request.return_value = mock.MagicMock( + ok=True, status_code=http_client.OK, + headers={"Content-Type": "application/pkix-cert"}, + content=b"hi") + # pylint: disable=protected-access + self.net._send_request('HEAD', 'http://example.com/', 'foo', bar='baz') + mock_logger.debug.assert_called_once_with( + 'Received response:\nHTTP %d\n%s\n\n%s', 200, + 'Content-Type: application/pkix-cert', b'aGk=') + + def test_send_request_post(self): + self.net.session = mock.MagicMock() + self.net.session.request.return_value = self.response + # pylint: disable=protected-access + self.assertEqual(self.response, self.net._send_request( + 'POST', 'http://example.com/', 'foo', data='qux', bar='baz')) + self.net.session.request.assert_called_once_with( + 'POST', 'http://example.com/', 'foo', + headers=mock.ANY, verify=mock.ANY, data='qux', bar='baz') + def test_send_request_verify_ssl(self): # pylint: disable=protected-access for verify in True, False: diff --git a/acme/acme/jose/json_util.py b/acme/acme/jose/json_util.py index da38b55ba..cc66d77ff 100644 --- a/acme/acme/jose/json_util.py +++ b/acme/acme/jose/json_util.py @@ -253,10 +253,6 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable): raise errors.SerializationError( 'Could not encode {0} ({1}): {2}'.format( slot, value, error)) - if omitted: - # pylint: disable=star-args - logger.debug('Omitted empty fields: %s', ', '.join( - '{0!s}={1!r}'.format(*field) for field in omitted)) return jobj def to_partial_json(self): diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 935d44994..8d01ad763 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -32,6 +32,6 @@ certbot_test_no_force_renew () { --agree-tos \ --register-unsafely-without-email \ --debug \ - -vvvvvvv \ + -vv \ "$@" } From 981d59fb451cd6634cdebb8754cb3c4a5fb97b34 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 27 Oct 2016 17:23:21 -0700 Subject: [PATCH 235/331] Specify archive directory in renewal configuration file (#3661) * Switch to using absolute path in symlink * save archive_dir to config and read it back * cli_config.archive_dir --> cli_config.default_archive_dir * Use archive_dir specified in renewal config file * add helpful broken symlink info * add docstring to method * Add tests * remove extraneous test imports * fix tests * py2.6 syntax fix * git problems * no dict comprehension in python2.6 * add test coverage * More py26 wrangling --- certbot/cert_manager.py | 22 ++++++ certbot/cli.py | 4 +- certbot/configuration.py | 2 +- certbot/main.py | 9 ++- certbot/storage.py | 65 +++++++++++------- certbot/tests/cert_manager_test.py | 101 ++++++++++++++++++++++++++++ certbot/tests/cli_test.py | 5 ++ certbot/tests/configuration_test.py | 4 +- certbot/tests/storage_test.py | 24 +++++-- 9 files changed, 203 insertions(+), 33 deletions(-) create mode 100644 certbot/cert_manager.py create mode 100644 certbot/tests/cert_manager_test.py diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py new file mode 100644 index 000000000..94714bc98 --- /dev/null +++ b/certbot/cert_manager.py @@ -0,0 +1,22 @@ +"""Tools for managing certificates.""" +from certbot import configuration +from certbot import renewal +from certbot import storage + +def update_live_symlinks(config): + """Update the certificate file family symlinks to use archive_dir. + + Use the information in the config file to make symlinks point to + the correct archive directory. + + .. note:: This assumes that the installation is using a Reverter object. + + :param config: Configuration. + :type config: :class:`certbot.interfaces.IConfig` + + """ + renewer_config = configuration.RenewerConfiguration(config) + for renewal_file in renewal.renewal_conf_files(renewer_config): + storage.RenewableCert(renewal_file, + configuration.RenewerConfiguration(renewer_config), + update_symlinks=True) diff --git a/certbot/cli.py b/certbot/cli.py index f840300fc..c27a278f3 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -67,6 +67,7 @@ cert. Major SUBCOMMANDS are: register Perform tasks related to registering with the CA rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation + update_symlinks Update cert symlinks based on renewal config file plugins Display information about installed plugins """.format(cli_command) @@ -322,7 +323,7 @@ class HelpfulArgumentParser(object): "install": main.install, "plugins": main.plugins_cmd, "register": main.register, "renew": main.renew, "revoke": main.revoke, "rollback": main.rollback, - "everything": main.run} + "everything": main.run, "update_symlinks": main.update_symlinks} # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) @@ -680,7 +681,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains " "as a parameter.") - helpful.add( [None, "testing", "renew", "certonly"], "--dry-run", action="store_true", dest="dry_run", diff --git a/certbot/configuration.py b/certbot/configuration.py index 712135b8d..1d4243272 100644 --- a/certbot/configuration.py +++ b/certbot/configuration.py @@ -96,7 +96,7 @@ class RenewerConfiguration(object): return getattr(self.namespace, name) @property - def archive_dir(self): # pylint: disable=missing-docstring + def default_archive_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR) @property diff --git a/certbot/main.py b/certbot/main.py index aa9b1892f..b651249fa 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -16,6 +16,7 @@ from acme import messages import certbot from certbot import account +from certbot import cert_manager from certbot import client from certbot import cli from certbot import crypto_util @@ -475,6 +476,13 @@ def config_changes(config, unused_plugins): """ client.view_config_changes(config, num=config.num) +def update_symlinks(config, unused_plugins): + """Update the certificate file family symlinks + + Use the information in the config file to make symlinks point to + the correct archive directory. + """ + cert_manager.update_live_symlinks(config) def revoke(config, unused_plugins): # TODO: coop with renewal config """Revoke a previously obtained certificate.""" @@ -540,7 +548,6 @@ def _csr_obtain_cert(config, le_client): certr, chain, config.cert_path, config.chain_path, config.fullchain_path) _report_new_cert(config, cert_path, cert_fullchain) - def obtain_cert(config, plugins, lineage=None): """Authenticate & obtain cert, but do not install it. diff --git a/certbot/storage.py b/certbot/storage.py index c740657d8..2134cd90b 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -54,11 +54,12 @@ def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()): return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0] -def write_renewal_config(o_filename, n_filename, target, relevant_data): +def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_data): """Writes a renewal config file with the specified name and values. :param str o_filename: Absolute path to the previous version of config file :param str n_filename: Absolute path to the new destination of config file + :param str archive_dir: Absolute path to the archive directory :param dict target: Maps ALL_FOUR to their symlink paths :param dict relevant_data: Renewal configuration options to save @@ -68,6 +69,7 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data): """ config = configobj.ConfigObj(o_filename) config["version"] = certbot.__version__ + config["archive_dir"] = archive_dir for kind in ALL_FOUR: config[kind] = target[kind] @@ -95,10 +97,11 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data): return config -def update_configuration(lineagename, target, cli_config): +def update_configuration(lineagename, archive_dir, target, cli_config): """Modifies lineagename's config to contain the specified values. :param str lineagename: Name of the lineage being modified + :param str archive_dir: Absolute path to the archive directory :param dict target: Maps ALL_FOUR to their symlink paths :param .RenewerConfiguration cli_config: parsed command line arguments @@ -117,7 +120,7 @@ def update_configuration(lineagename, target, cli_config): # Save only the config items that are relevant to renewal values = relevant_values(vars(cli_config.namespace)) - write_renewal_config(config_filename, temp_filename, target, values) + write_renewal_config(config_filename, temp_filename, archive_dir, target, values) os.rename(temp_filename, config_filename) return configobj.ConfigObj(config_filename) @@ -204,7 +207,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes renewal configuration file and/or systemwide defaults. """ - def __init__(self, config_filename, cli_config): + def __init__(self, config_filename, cli_config, update_symlinks=False): """Instantiate a RenewableCert object from an existing lineage. :param str config_filename: the path to the renewal config file @@ -256,8 +259,19 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes self.live_dir = os.path.dirname(self.cert) self._fix_symlinks() + if update_symlinks: + self._update_symlinks() self._check_symlinks() + @property + def archive_dir(self): + """Returns the default or specified archive directory""" + if "archive_dir" in self.configuration: + return self.configuration["archive_dir"] + else: + return os.path.join( + self.cli_config.default_archive_dir, self.lineagename) + def _check_symlinks(self): """Raises an exception if a symlink doesn't exist""" for kind in ALL_FOUR: @@ -270,6 +284,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes raise errors.CertStorageError("target {0} of symlink {1} does " "not exist".format(target, link)) + def _update_symlinks(self): + """Updates symlinks to use archive_dir""" + for kind in ALL_FOUR: + link = getattr(self, kind) + previous_link = get_link_target(link) + new_link = os.path.join(self.archive_dir, os.path.basename(previous_link)) + + os.unlink(link) + os.symlink(new_link, link) + def _consistent(self): """Are the files associated with this lineage self-consistent? @@ -297,16 +321,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Each element's link must point within the cert lineage's # directory within the official archive directory - desired_directory = os.path.join( - self.cli_config.archive_dir, self.lineagename) - if not os.path.samefile(os.path.dirname(target), - desired_directory): + if not os.path.samefile(os.path.dirname(target), self.archive_dir): logger.debug("Element's link does not point within the " "cert lineage's directory within the " "official archive directory. Link: %s, " "target directory: %s, " - "archive directory: %s.", - link, os.path.dirname(target), desired_directory) + "archive directory: %s. If you've specified " + "the archive directory in the renewal configuration " + "file, you may need to update links by running " + "certbot update_symlinks.", + link, os.path.dirname(target), self.archive_dir) return False # The link must point to a file that exists @@ -759,7 +783,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes """ # Examine the configuration and find the new lineage's name - for i in (cli_config.renewal_configs_dir, cli_config.archive_dir, + for i in (cli_config.renewal_configs_dir, cli_config.default_archive_dir, cli_config.live_dir): if not os.path.exists(i): os.makedirs(i, 0o700) @@ -774,7 +798,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # lineagename will now potentially be modified based on which # renewal configuration file could actually be created lineagename = os.path.basename(config_filename)[:-len(".conf")] - archive = os.path.join(cli_config.archive_dir, lineagename) + archive = os.path.join(cli_config.default_archive_dir, lineagename) live_dir = os.path.join(cli_config.live_dir, lineagename) if os.path.exists(archive): raise errors.CertStorageError( @@ -786,13 +810,12 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes os.mkdir(live_dir) logger.debug("Archive directory %s and live " "directory %s created.", archive, live_dir) - relative_archive = os.path.join("..", "..", "archive", lineagename) # Put the data into the appropriate files on disk target = dict([(kind, os.path.join(live_dir, kind + ".pem")) for kind in ALL_FOUR]) for kind in ALL_FOUR: - os.symlink(os.path.join(relative_archive, kind + "1.pem"), + os.symlink(os.path.join(archive, kind + "1.pem"), target[kind]) with open(target["cert"], "w") as f: logger.debug("Writing certificate to %s.", target["cert"]) @@ -816,7 +839,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Save only the config items that are relevant to renewal values = relevant_values(vars(cli_config.namespace)) - new_config = write_renewal_config(config_filename, config_filename, target, values) + new_config = write_renewal_config(config_filename, config_filename, archive, + target, values) return cls(new_config.filename, cli_config) def save_successor(self, prior_version, new_cert, @@ -851,14 +875,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes self.cli_config = cli_config target_version = self.next_free_version() - archive = self.cli_config.archive_dir - # XXX if anyone ever moves a renewal configuration file, this will - # break... perhaps prefix should be the dirname of the previous - # cert.pem? - prefix = os.path.join(archive, self.lineagename) target = dict( [(kind, - os.path.join(prefix, "{0}{1}.pem".format(kind, target_version))) + os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version))) for kind in ALL_FOUR]) # Distinguish the cases where the privkey has changed and where it @@ -868,7 +887,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # The behavior below keeps the prior key by creating a new # symlink to the old key or the target of the old key symlink. old_privkey = os.path.join( - prefix, "privkey{0}.pem".format(prior_version)) + self.archive_dir, "privkey{0}.pem".format(prior_version)) if os.path.islink(old_privkey): old_privkey = os.readlink(old_privkey) else: @@ -894,7 +913,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes symlinks = dict((kind, self.configuration[kind]) for kind in ALL_FOUR) # Update renewal config file self.configfile = update_configuration( - self.lineagename, symlinks, cli_config) + self.lineagename, self.archive_dir, symlinks, cli_config) self.configuration = config_with_defaults(self.configfile) return target_version diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py new file mode 100644 index 000000000..46f555aac --- /dev/null +++ b/certbot/tests/cert_manager_test.py @@ -0,0 +1,101 @@ +"""Tests for certbot.cert_manager.""" +# pylint disable=protected-access +import os +import shutil +import tempfile +import unittest + +import configobj +import mock + +from certbot import configuration +from certbot.storage import ALL_FOUR + +class CertManagerTest(unittest.TestCase): + """Tests for certbot.cert_manager + """ + def setUp(self): + self.tempdir = tempfile.mkdtemp() + + os.makedirs(os.path.join(self.tempdir, "renewal")) + + mock_namespace = mock.MagicMock( + config_dir=self.tempdir, + work_dir=self.tempdir, + logs_dir=self.tempdir, + ) + + self.cli_config = configuration.RenewerConfiguration( + namespace=mock_namespace + ) + + self.domains = { + "example.org": None, + "other.com": os.path.join(self.tempdir, "specialarchive") + } + self.configs = dict((domain, self._set_up_config(domain, self.domains[domain])) + for domain in self.domains) + + # We also create a file that isn't a renewal config in the same + # location to test that logic that reads in all-and-only renewal + # configs will ignore it and NOT attempt to parse it. + junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w") + junk.write("This file should be ignored!") + junk.close() + + def _set_up_config(self, domain, custom_archive): + # TODO: maybe provide RenewerConfiguration.make_dirs? + # TODO: main() should create those dirs, c.f. #902 + os.makedirs(os.path.join(self.tempdir, "live", domain)) + config = configobj.ConfigObj() + + if custom_archive is not None: + os.makedirs(custom_archive) + config["archive_dir"] = custom_archive + else: + os.makedirs(os.path.join(self.tempdir, "archive", domain)) + + for kind in ALL_FOUR: + config[kind] = os.path.join(self.tempdir, "live", domain, + kind + ".pem") + + config.filename = os.path.join(self.tempdir, "renewal", + domain + ".conf") + config.write() + return config + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def test_update_live_symlinks(self): + """Test update_live_symlinks""" + # pylint: disable=too-many-statements + # create files with incorrect symlinks + from certbot import cert_manager + archive_paths = {} + for domain in self.domains: + custom_archive = self.domains[domain] + if custom_archive is not None: + archive_dir_path = custom_archive + else: + archive_dir_path = os.path.join(self.tempdir, "archive", domain) + archive_paths[domain] = dict((kind, + os.path.join(archive_dir_path, kind + "1.pem")) for kind in ALL_FOUR) + for kind in ALL_FOUR: + live_path = self.configs[domain][kind] + archive_path = archive_paths[domain][kind] + open(archive_path, 'a').close() + # path is incorrect but base must be correct + os.symlink(os.path.join(self.tempdir, kind + "1.pem"), live_path) + + # run update symlinks + cert_manager.update_live_symlinks(self.cli_config) + + # check that symlinks go where they should + for domain in self.domains: + for kind in ALL_FOUR: + self.assertEqual(os.readlink(self.configs[domain][kind]), + archive_paths[domain][kind]) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index ce01d69ff..63abe6451 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -272,6 +272,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods _, _, _, client = self._call(['config_changes']) self.assertEqual(1, client.view_config_changes.call_count) + @mock.patch('certbot.cert_manager.update_live_symlinks') + def test_update_symlinks(self, mock_cert_manager): + self._call_no_clientmock(['update_symlinks']) + self.assertEqual(1, mock_cert_manager.call_count) + def test_plugins(self): flags = ['--init', '--prepare', '--authenticators', '--installers'] for args in itertools.chain( diff --git a/certbot/tests/configuration_test.py b/certbot/tests/configuration_test.py index 211a0eae6..5e59d0b86 100644 --- a/certbot/tests/configuration_test.py +++ b/certbot/tests/configuration_test.py @@ -104,7 +104,7 @@ class RenewerConfigurationTest(unittest.TestCase): constants.RENEWAL_CONFIGS_DIR = 'renewal_configs' constants.RENEWER_CONFIG_FILENAME = 'r.conf' - self.assertEqual(self.config.archive_dir, '/tmp/config/a') + self.assertEqual(self.config.default_archive_dir, '/tmp/config/a') self.assertEqual(self.config.live_dir, '/tmp/config/l') self.assertEqual( self.config.renewal_configs_dir, '/tmp/config/renewal_configs') @@ -127,7 +127,7 @@ class RenewerConfigurationTest(unittest.TestCase): mock_namespace.logs_dir = logs_base config = RenewerConfiguration(NamespaceConfig(mock_namespace)) - self.assertTrue(os.path.isabs(config.archive_dir)) + self.assertTrue(os.path.isabs(config.default_archive_dir)) self.assertTrue(os.path.isabs(config.live_dir)) self.assertTrue(os.path.isabs(config.renewal_configs_dir)) self.assertTrue(os.path.isabs(config.renewer_config_file)) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 9566e0aec..bfbcd885e 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -593,7 +593,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "the-lineage.com", "cert3", "privkey3", "chain3", self.cli_config) - os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com")) + os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "other-example.com", "cert4", @@ -613,7 +613,7 @@ class RenewableCertTests(BaseRenewableCertTest): from certbot import storage shutil.rmtree(self.cli_config.renewal_configs_dir) - shutil.rmtree(self.cli_config.archive_dir) + shutil.rmtree(self.cli_config.default_archive_dir) shutil.rmtree(self.cli_config.live_dir) storage.RenewableCert.new_lineage( @@ -624,7 +624,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertTrue(os.path.exists(os.path.join( self.cli_config.live_dir, "the-lineage.com", "privkey.pem"))) self.assertTrue(os.path.exists(os.path.join( - self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem"))) + self.cli_config.default_archive_dir, "the-lineage.com", "privkey1.pem"))) @mock.patch("certbot.storage.util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): @@ -721,9 +721,10 @@ class RenewableCertTests(BaseRenewableCertTest): target = {} for x in ALL_FOUR: target[x] = "somewhere" + archive_dir = "the_archive" relevant_data = {"useful": "new_value"} from certbot import storage - storage.write_renewal_config(temp, temp2, target, relevant_data) + storage.write_renewal_config(temp, temp2, archive_dir, target, relevant_data) with open(temp2, "r") as f: content = f.read() # useful value was updated @@ -735,6 +736,21 @@ class RenewableCertTests(BaseRenewableCertTest): # check version was stored self.assertTrue("version = {0}".format(certbot.__version__) in content) + def test_update_symlinks(self): + from certbot import storage + archive_dir_path = os.path.join(self.tempdir, "archive", "example.org") + for kind in ALL_FOUR: + live_path = self.config[kind] + basename = kind + "1.pem" + archive_path = os.path.join(archive_dir_path, basename) + open(archive_path, 'a').close() + os.symlink(os.path.join(self.tempdir, basename), live_path) + self.assertRaises(errors.CertStorageError, + storage.RenewableCert, self.config.filename, + self.cli_config) + storage.RenewableCert(self.config.filename, self.cli_config, + update_symlinks=True) + if __name__ == "__main__": unittest.main() # pragma: no cover From 88a2c5a8f6e94111bfc6795562bfa409a9b1b15f Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 28 Oct 2016 19:50:07 +0200 Subject: [PATCH 236/331] Testing the output of build.py against lea-source/lea (#3460) * Testing the output of build.py against lea-source/lea * Replacing realpath with readlink --- tests/modification-check.sh | 40 +++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/modification-check.sh b/tests/modification-check.sh index 53a5efa93..73cdb0c09 100755 --- a/tests/modification-check.sh +++ b/tests/modification-check.sh @@ -3,6 +3,13 @@ temp_dir=`mktemp -d` # Script should be run from Certbot's root directory + +SCRIPT_PATH=`dirname $0` +SCRIPT_PATH=`readlink -f $SCRIPT_PATH` +FLAG=false + +# Compare root letsencrypt-auto and certbot-auto with published versions + cp letsencrypt-auto ${temp_dir}/letsencrypt-to-be-checked cp certbot-auto ${temp_dir}/certbot-to-be-checked @@ -16,8 +23,7 @@ cmp -s letsencrypt-auto letsencrypt-to-be-checked if [ $? != 0 ]; then echo "Root letsencrypt-auto has changed." - rm -rf temp_dir - exit 1 + FLAG=true else echo "Root letsencrypt-auto is unchanged." fi @@ -26,10 +32,36 @@ cmp -s letsencrypt-auto certbot-to-be-checked if [ $? != 0 ]; then echo "Root certbot-auto has changed." - rm -rf temp_dir - exit 1 + FLAG=true else echo "Root certbot-auto is unchanged." fi +# Cleanup +rm ${temp_dir}/* +cd ${SCRIPT_PATH}/../ + +# Compare letsencrypt-auto-source/letsencrypt-auto with output of build.py + +cp letsencrypt-auto-source/letsencrypt-auto ${temp_dir}/original-lea +python letsencrypt-auto-source/build.py +cp letsencrypt-auto-source/letsencrypt-auto ${temp_dir}/build-lea + +cd $temp_dir + +cmp -s original-lea build-lea + +if [ $? != 0 ]; then + echo "letsencrypt-auto-source/letsencrypt-auto doesn't match output of \ +build.py." + FLAG=true +else + echo "letsencrypt-auto-source/letsencrypt-auto matches output of \ +build.py." +fi + +if $FLAG ; then + exit 1 +fi + rm -rf temp_dir From 5ed0f3610c276dbd81de5057f9dab92a467fb632 Mon Sep 17 00:00:00 2001 From: tcottier Date: Fri, 28 Oct 2016 20:18:56 +0200 Subject: [PATCH 237/331] When getopts is called multiple time we need to reset OPTIND. (#3475) Not resetting OPTIND between each call of getopts skips all short args except the first one. It fixes this automated command: ./certbot-auto certonly --webroot -w /tmp -d example.com --agree-tos --email contact@example.com -n Where "-w" was parsed by getopts and not "-n" * When getopts is called multiple time we need to reset OPTIND. Issue #3459 * Adding OPTIND reset in the certbot-auto source file * Building new letsencrypt-auto from template --- letsencrypt-auto-source/letsencrypt-auto | 1 + letsencrypt-auto-source/letsencrypt-auto.template | 1 + 2 files changed, 2 insertions(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 4ec226d39..58e00931a 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -58,6 +58,7 @@ for arg in "$@" ; do --verbose) VERBOSE=1;; -[!-]*) + OPTIND=1 while getopts ":hnvq" short_arg $arg; do case "$short_arg" in h) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 991d9dd76..2ac8d8d79 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -58,6 +58,7 @@ for arg in "$@" ; do --verbose) VERBOSE=1;; -[!-]*) + OPTIND=1 while getopts ":hnvq" short_arg $arg; do case "$short_arg" in h) From 3534e4cb1f615e87db22321c742422d343779a3c Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Sat, 29 Oct 2016 01:05:25 +0300 Subject: [PATCH 238/331] Allowing modification check to run using "tox" (#3704) #3337 and #3338 should ideally run when the user type tox. This allows them to catch the problem locally before they make a PR. --- tox.ini | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 162601205..7de8fb9bb 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ [tox] skipsdist = true -envlist = py{26,33,34,35},cover,lint +envlist = modification,py{26,33,34,35},cover,lint # nosetest -v => more verbose output, allows to detect busy waiting # loops, especially on Travis @@ -93,6 +93,12 @@ commands = pip install -e acme[dev] -e .[dev] -e certbot-nginx python certbot-compatibility-test/nginx/roundtrip.py certbot-compatibility-test/nginx/nginx-roundtrip-testdata +# This is a duplication of the command line in testenv:le_auto to +# allow users to run the modification check by running `tox` +[testenv:modification] +commands = + {toxinidir}/tests/modification-check.sh + [testenv:le_auto] # At the moment, this tests under Python 2.7 only, as only that version is # readily available on the Trusty Docker image. From 30dd22f2f8bf3e82786ebd25ace2d1a67fe157eb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 31 Oct 2016 18:30:02 -0700 Subject: [PATCH 239/331] No doc,dev depedencies for compatibility-test (#3722) --- certbot-compatibility-test/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot-compatibility-test/Dockerfile b/certbot-compatibility-test/Dockerfile index e445a3555..c8ef62696 100644 --- a/certbot-compatibility-test/Dockerfile +++ b/certbot-compatibility-test/Dockerfile @@ -38,8 +38,7 @@ RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \ -e /opt/certbot/src \ -e /opt/certbot/src/certbot-apache \ -e /opt/certbot/src/certbot-nginx \ - -e /opt/certbot/src/certbot-compatibility-test \ - -e /opt/certbot/src[dev,docs] + -e /opt/certbot/src/certbot-compatibility-test # install in editable mode (-e) to save space: it's not possible to # "rm -rf /opt/certbot/src" (it's stays in the underlaying image); From 2564fb785b7d174cb8a4ced964c63184b3ae1a61 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 1 Nov 2016 14:25:26 -0700 Subject: [PATCH 240/331] I restructured Installation and Using a bit (#3725) * Fixing a weird out-of-place paragraph in the Getting Certbot section * De-duping and clarifying installation information, separating it from Using. * Responding to feedback at https://github.com/certbot/certbot/pull/3675#pullrequestreview-5757007 --- README.rst | 2 +- docs/install.rst | 213 +++++++++++++++++++++++++++++++++++++++++++--- docs/using.rst | 217 ++--------------------------------------------- 3 files changed, 205 insertions(+), 227 deletions(-) diff --git a/README.rst b/README.rst index 244b6b510..f986703ac 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ Installation The easiest way to install Certbot is by visiting `certbot.eff.org`_, where you can find the correct installation instructions for many web server and OS combinations. -For more information, see the `User Guide `_. +For more information, see `Get Certbot `_. .. _certbot.eff.org: https://certbot.eff.org/ diff --git a/docs/install.rst b/docs/install.rst index e79a3b596..182abdb71 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,13 +1,57 @@ ===================== -Quick Installation +Get Certbot ===================== -If ``certbot`` (or ``letsencrypt``) is packaged for your Unix OS (visit -certbot.eff.org_ to find out), you can install it -from there, and run it by typing ``certbot`` (or ``letsencrypt``). Because -not all operating systems have packages yet, we provide a temporary solution -via the ``certbot-auto`` wrapper script, which obtains some dependencies from -your OS and puts others in a python virtual environment:: +.. contents:: Table of Contents + :local: + + +About Certbot +============= + +Certbot is packaged for many common operating systems and web servers. Check whether +``certbot`` (or ``letsencrypt``) is packaged for your web server's OS by visiting +certbot.eff.org_, where you will also find the correct installation instructions for +your system. + +.. Note:: Unless you have very specific requirements, we kindly suggest that you use the Certbot packages provided by your package manager (see certbot.eff.org_). If such packages are not available, we recommend using ``certbot-auto``, which automates the process of installing Certbot on your system. + +.. _certbot.eff.org: https://certbot.eff.org + + +System Requirements +=================== + +The Let's Encrypt Client presently only runs on Unix-ish OSes that include +Python 2.6 or 2.7; Python 3.x support will hopefully be added in the future. The +client requires root access in order to write to ``/etc/letsencrypt``, +``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443 +(if you use the ``standalone`` plugin) and to read and modify webserver +configurations (if you use the ``apache`` or ``nginx`` plugins). If none of +these apply to you, it is theoretically possible to run without root privileges, +but for most users who want to avoid running an ACME client as root, either +`letsencrypt-nosudo `_ or +`simp_le `_ are more appropriate choices. + +The Apache plugin currently requires OS with augeas version 1.0; currently `it +supports +`_ +modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin. + +Alternate installation methods +================================ + +If you are offline or your operating system doesn't provide a package, you can use +an alternate method for installing ``certbot``. + +.. _certbot-auto: + +Certbot-Auto +------------ + +The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies +from your web server OS and putting others in a python virtual environment. You can +download and run it as follows:: user@webserver:~$ wget https://dl.eff.org/certbot-auto user@webserver:~$ chmod a+x ./certbot-auto @@ -20,14 +64,155 @@ your OS and puts others in a python virtual environment:: user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc certbot-auto -And for full command line help, you can type:: +The ``certbot-auto`` command updates to the latest client release automatically. +Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly +the same command line flags and arguments. For more information, see +`Certbot command-line options `_. + +For full command line help, you can type:: ./certbot-auto --help all -``certbot-auto`` updates to the latest client release automatically. And -since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly -the same command line flags and arguments. More details about this script and -other installation methods can be found `in the User Guide -`_. +Running with Docker +------------------- + +Docker_ is an amazingly simple and quick way to obtain a +certificate. However, this mode of operation is unable to install +certificates or configure your webserver, because our installer +plugins cannot reach your webserver from inside the Docker container. + +Most users should use the operating system packages (see instructions at +certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only +use Docker if you are sure you know what you are doing and have a +good reason to do so. + +You should definitely read the :ref:`where-certs` section, in order to +know how to manage the certs +manually. `Our ciphersuites page `__ +provides some information about recommended ciphersuites. If none of +these make much sense to you, you should definitely use the +certbot-auto_ method, which enables you to use installer plugins +that cover both of those hard topics. + +If you're still not convinced and have decided to use this method, +from the server that the domain you're requesting a cert for resolves +to, `install Docker`_, then issue the following command: + +.. code-block:: shell + + sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot \ + -v "/etc/letsencrypt:/etc/letsencrypt" \ + -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ + quay.io/letsencrypt/letsencrypt:latest certonly + +Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory +``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from +within Docker, you must install the certificate manually according to the procedure +recommended by the provider of your webserver. + +For more information about the layout +of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. + +.. _Docker: https://docker.com +.. _`install Docker`: https://docs.docker.com/engine/installation/ + +Operating System Packages +------------------------- + +**FreeBSD** + + * Port: ``cd /usr/ports/security/py-certbot && make install clean`` + * Package: ``pkg install py27-certbot`` + +**OpenBSD** + + * Port: ``cd /usr/ports/security/letsencrypt/client && make install clean`` + * Package: ``pkg_add letsencrypt`` + +**Arch Linux** + +.. code-block:: shell + + sudo pacman -S certbot + +**Debian** + +If you run Debian Stretch or Debian Sid, you can install certbot packages. + +.. code-block:: shell + + sudo apt-get update + sudo apt-get install certbot python-certbot-apache + +If you don't want to use the Apache plugin, you can omit the +``python-certbot-apache`` package. + +Packages exist for Debian Jessie via backports. First you'll have to follow the +instructions at http://backports.debian.org/Instructions/ to enable the Jessie backports +repo, if you have not already done so. Then run: + +.. code-block:: shell + + sudo apt-get install letsencrypt python-letsencrypt-apache -t jessie-backports + +**Fedora** + +.. code-block:: shell + + sudo dnf install letsencrypt + +**Gentoo** + +The official Certbot client is available in Gentoo Portage. If you +want to use the Apache plugin, it has to be installed separately: + +.. code-block:: shell + + emerge -av app-crypt/letsencrypt + emerge -av app-crypt/letsencrypt-apache + +When using the Apache plugin, you will run into a "cannot find a cert or key +directive" error if you're sporting the default Gentoo ``httpd.conf``. +You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf`` +as follows: + +Change + +.. code-block:: shell + + + LoadModule ssl_module modules/mod_ssl.so + + +to + +.. code-block:: shell + + # + LoadModule ssl_module modules/mod_ssl.so + # + +For the time being, this is the only way for the Apache plugin to recognise +the appropriate directives when installing the certificate. +Note: this change is not required for the other plugins. + +**Other Operating Systems** + +OS packaging is an ongoing effort. If you'd like to package +Certbot for your distribution of choice please have a +look at the :doc:`packaging`. + +Installing from source +---------------------- + +Installation from source is only supported for developers and the +whole process is described in the :doc:`contributing`. + +.. warning:: Please do **not** use ``python setup.py install`` or + ``python pip install .``. Please do **not** attempt the + installation commands as superuser/root and/or without virtual + environment, e.g. ``sudo python setup.py install``, ``sudo pip + install``, ``sudo ./venv/bin/...``. These modes of operation might + corrupt your operating system and are **not supported** by the + Certbot team! -.. _certbot.eff.org: https://certbot.eff.org/ diff --git a/docs/using.rst b/docs/using.rst index 676364117..1becea8ea 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -5,44 +5,10 @@ User Guide .. contents:: Table of Contents :local: -.. _installation: +Certbot Commands +================ -System Requirements -=================== - -The Let's Encrypt Client presently only runs on Unix-ish OSes that include -Python 2.6 or 2.7; Python 3.x support will hopefully be added in the future. The -client requires root access in order to write to ``/etc/letsencrypt``, -``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443 -(if you use the ``standalone`` plugin) and to read and modify webserver -configurations (if you use the ``apache`` or ``nginx`` plugins). If none of -these apply to you, it is theoretically possible to run without root privileges, -but for most users who want to avoid running an ACME client as root, either -`letsencrypt-nosudo `_ or -`simp_le `_ are more appropriate choices. - -The Apache plugin currently requires OS with augeas version 1.0; currently `it -supports -`_ -modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin. - - -Getting Certbot -=============== -Certbot is packaged for many common operating systems and web servers. Check whether -``certbot`` (or ``letsencrypt``) is packaged for your web server's OS by visiting -certbot.eff.org_, where you will also find the correct installation instructions for -your system. - -.. Note:: Unless you have very specific requirements, we kindly suggest that you use the Certbot packages provided by your package manager (see certbot.eff.org_). If such packages are not available, we recommend using ``certbot-auto``, which automates the process of installing Certbot on your system. -.. _certbot.eff.org: https://certbot.eff.org - -.. _certbot-auto: https://certbot.eff.org/docs/using.html#certbot-auto - -Commands -======== - -The Certbot client uses a number of different "commands" (also referred +Certbot uses a number of different "commands" (also referred to, equivalently, as "subcommands") to request specific actions such as obtaining, renewing, or revoking certificates. Some of the most important and most commonly-used commands will be discussed throughout this @@ -50,183 +16,10 @@ document; an exhaustive list also appears near the end of the document. The ``certbot`` script on your web server might be named ``letsencrypt`` if your system uses an older package, or ``certbot-auto`` if you used an alternate installation method. Throughout the docs, whenever you see ``certbot``, swap in the correct name as needed. - -Other installation methods --------------------------- -If you are offline or your operating system doesn't provide a package, you can use -an alternate method for installing ``certbot``. - -Certbot-Auto -^^^^^^^^^^^^ -The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies -from your web server OS and putting others in a python virtual environment. You can -download and run it as follows:: - - user@webserver:~$ wget https://dl.eff.org/certbot-auto - user@webserver:~$ chmod a+x ./certbot-auto - user@webserver:~$ ./certbot-auto --help - -.. hint:: The certbot-auto download is protected by HTTPS, which is pretty good, but if you'd like to - double check the integrity of the ``certbot-auto`` script, you can use these steps for verification before running it:: - - user@server:~$ wget -N https://dl.eff.org/certbot-auto.asc - user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2 - user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc certbot-auto - -The ``certbot-auto`` command updates to the latest client release automatically. -Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly -the same command line flags and arguments. For more information, see -`Certbot command-line options `_. - -Running with Docker -^^^^^^^^^^^^^^^^^^^ - -Docker_ is an amazingly simple and quick way to obtain a -certificate. However, this mode of operation is unable to install -certificates or configure your webserver, because our installer -plugins cannot reach your webserver from inside the Docker container. - -Most users should use the operating system packages (see instructions at -certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only -use Docker if you are sure you know what you are doing and have a -good reason to do so. - -You should definitely read the :ref:`where-certs` section, in order to -know how to manage the certs -manually. `Our ciphersuites page `__ -provides some information about recommended ciphersuites. If none of -these make much sense to you, you should definitely use the -certbot-auto_ method, which enables you to use installer plugins -that cover both of those hard topics. - -If you're still not convinced and have decided to use this method, -from the server that the domain you're requesting a cert for resolves -to, `install Docker`_, then issue the following command: - -.. code-block:: shell - - sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot \ - -v "/etc/letsencrypt:/etc/letsencrypt" \ - -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ - quay.io/letsencrypt/letsencrypt:latest certonly - -Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory -``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from -within Docker, you must install the certificate manually according to the procedure -recommended by the provider of your webserver. - -For more information about the layout -of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. - -.. _Docker: https://docker.com -.. _`install Docker`: https://docs.docker.com/engine/installation/ - - -Operating System Packages -^^^^^^^^^^^^^^^^^^^^^^^^^ - -**FreeBSD** - - * Port: ``cd /usr/ports/security/py-certbot && make install clean`` - * Package: ``pkg install py27-certbot`` - -**OpenBSD** - - * Port: ``cd /usr/ports/security/letsencrypt/client && make install clean`` - * Package: ``pkg_add letsencrypt`` - -**Arch Linux** - -.. code-block:: shell - - sudo pacman -S certbot - -**Debian** - -If you run Debian Stretch or Debian Sid, you can install certbot packages. - -.. code-block:: shell - - sudo apt-get update - sudo apt-get install certbot python-certbot-apache - -If you don't want to use the Apache plugin, you can omit the -``python-certbot-apache`` package. - -Packages exist for Debian Jessie via backports. First you'll have to follow the -instructions at http://backports.debian.org/Instructions/ to enable the Jessie backports -repo, if you have not already done so. Then run: - -.. code-block:: shell - - sudo apt-get install letsencrypt python-letsencrypt-apache -t jessie-backports - -**Fedora** - -.. code-block:: shell - - sudo dnf install letsencrypt - -**Gentoo** - -The official Certbot client is available in Gentoo Portage. If you -want to use the Apache plugin, it has to be installed separately: - -.. code-block:: shell - - emerge -av app-crypt/letsencrypt - emerge -av app-crypt/letsencrypt-apache - -When using the Apache plugin, you will run into a "cannot find a cert or key -directive" error if you're sporting the default Gentoo ``httpd.conf``. -You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf`` -as follows: - -Change - -.. code-block:: shell - - - LoadModule ssl_module modules/mod_ssl.so - - -to - -.. code-block:: shell - - # - LoadModule ssl_module modules/mod_ssl.so - # - -For the time being, this is the only way for the Apache plugin to recognise -the appropriate directives when installing the certificate. -Note: this change is not required for the other plugins. - -**Other Operating Systems** - -OS packaging is an ongoing effort. If you'd like to package -Certbot for your distribution of choice please have a -look at the :doc:`packaging`. - - -Installing from source -^^^^^^^^^^^^^^^^^^^^^^ - -Installation from source is only supported for developers and the -whole process is described in the :doc:`contributing`. - -.. warning:: Please do **not** use ``python setup.py install`` or - ``python pip install .``. Please do **not** attempt the - installation commands as superuser/root and/or without virtual - environment, e.g. ``sudo python setup.py install``, ``sudo pip - install``, ``sudo ./venv/bin/...``. These modes of operation might - corrupt your operating system and are **not supported** by the - Certbot team! - .. _plugins: -Getting certificates (and chosing plugins) -========================================== +Getting certificates (and choosing plugins) +=========================================== The Certbot client supports a number of different "plugins" that can be used to obtain and/or install certificates. From db4c88856a5e977104d584328ad85f4569d56376 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 2 Nov 2016 16:14:14 -0700 Subject: [PATCH 241/331] Fix non-ASCII domain check. (#3727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix non-ASCII domain check. Previously, the code would convert to utf-8, check for non-ASCII, and then try to use .format() to interpolate the result into an error message. This would generate a second error that would cause the whole message to get dropped, and the program to silently exit. The problem can be succinctly observed like so: $ python >>> "{0}".format("ウェブ.crud.net") '\xe3\x82\xa6\xe3\x82\xa7\xe3\x83\x96.crud.net' >>> "{0}".format(u"ウェブ.crud.net") Traceback (most recent call last): File "", line 1, in UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordinal not in range(128) Note for the curious: This problem only seems to happen with .format(): >>> "%s" % ("ウェブ.crud.net") '\xe3\x82\xa6\xe3\x82\xa7\xe3\x83\x96.crud.net' >>> "%s" % (u"ウェブ.crud.net") u'\u30a6\u30a7\u30d6.crud.net' --- certbot/util.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/certbot/util.py b/certbot/util.py index 577180b00..f3a74d47d 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -451,12 +451,8 @@ def enforce_domain_sanity(domain): domain = domain.decode('utf-8') domain.encode('ascii') except UnicodeError: - error_fmt = (u"Internationalized domain names " - "are not presently supported: {0}") - if isinstance(domain, six.text_type): - raise errors.ConfigurationError(error_fmt.format(domain)) - else: - raise errors.ConfigurationError(str(error_fmt).format(domain)) + raise errors.ConfigurationError("Non-ASCII domain names not supported. " + "To issue for an Internationalized Domain Name, use Punycode.") domain = domain.lower() From 61094b06fdd7ff391d7c1d4402539d8b5d690d20 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 3 Nov 2016 17:19:53 -0700 Subject: [PATCH 242/331] Do we need trusty in Travis? (#3737) * do we need trusty? * add docker as a dependency for boulder?? --- .travis.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 373e16174..733b45f52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,6 @@ cache: directories: - $HOME/.cache/pip -# This makes sure we get a host with docker-compose present. -dist: trusty - before_install: - 'dpkg -s libaugeas0' @@ -21,30 +18,34 @@ matrix: include: - python: "2.7" env: TOXENV=cover BOULDER_INTEGRATION=1 - sudo: true + sudo: required after_failure: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql + services: docker - python: "2.7" env: TOXENV=lint - python: "2.7" env: TOXENV=py27-oldest BOULDER_INTEGRATION=1 - sudo: true + sudo: required after_failure: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql + services: docker - python: "2.6" env: TOXENV=py26 BOULDER_INTEGRATION=1 - sudo: true + sudo: required after_failure: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql + services: docker - python: "2.6" env: TOXENV=py26-oldest BOULDER_INTEGRATION=1 - sudo: true + sudo: required after_failure: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql + services: docker - sudo: required env: TOXENV=nginx_compat services: docker From ca9b3f18af1e43690db89082fe50213632d3de22 Mon Sep 17 00:00:00 2001 From: Jaap Eldering Date: Thu, 3 Nov 2016 23:13:02 -0200 Subject: [PATCH 243/331] Allow user to override sudo as root authorization method [minor revision requested] (#1969) * Move su_sudo() wrapper function outside of root method selection code. * Improve comment language. * Allow overriding root authorization mechanism (sudo/su/nothing) by setting LE_AUTO_SUDO environment variable. * Update generated letsencrypt-auto-source/letsencrypt-auto from template. * Add change requests from Brad Warren and regenerate letsencrypt-auto. Thanks for pointing out. --- letsencrypt-auto-source/letsencrypt-auto | 80 ++++++++++++------- .../letsencrypt-auto.template | 80 ++++++++++++------- 2 files changed, 100 insertions(+), 60 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 58e00931a..f021140ce 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -84,39 +84,59 @@ fi # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using -# `su` +# `su`. Auto-detection can be overrided by explicitly setting the +# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below. + +# Because the parameters in `su -c` has to be a string, +# we need to properly escape it. +su_sudo() { + args="" + # This `while` loop iterates over all parameters given to this function. + # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string + # will be wrapped in a pair of `'`, then appended to `$args` string + # For example, `echo "It's only 1\$\!"` will be escaped to: + # 'echo' 'It'"'"'s only 1$!' + # │ │└┼┘│ + # │ │ │ └── `'s only 1$!'` the literal string + # │ │ └── `\"'\"` is a single quote (as a string) + # │ └── `'It'`, to be concatenated with the strings following it + # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself + while [ $# -ne 0 ]; do + args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " + shift + done + su root -c "$args" +} + SUDO_ENV="" export CERTBOT_AUTO="$0" -if test "`id -u`" -ne "0" ; then - if command -v sudo 1>/dev/null 2>&1; then - SUDO=sudo - SUDO_ENV="CERTBOT_AUTO=$0" - else - echo \"sudo\" is not available, will use \"su\" for installation steps... - # Because the parameters in `su -c` has to be a string, - # we need properly escape it - su_sudo() { - args="" - # This `while` loop iterates over all parameters given to this function. - # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string - # will be wrapped in a pair of `'`, then appended to `$args` string - # For example, `echo "It's only 1\$\!"` will be escaped to: - # 'echo' 'It'"'"'s only 1$!' - # │ │└┼┘│ - # │ │ │ └── `'s only 1$!'` the literal string - # │ │ └── `\"'\"` is a single quote (as a string) - # │ └── `'It'`, to be concatenated with the strings following it - # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself - while [ $# -ne 0 ]; do - args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " - shift - done - su root -c "$args" - } - SUDO=su_sudo - fi +if [ -n "${LE_AUTO_SUDO:+x}" ]; then + case "$LE_AUTO_SUDO" in + su_sudo|su) + SUDO=su_sudo + ;; + sudo) + SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=$0" + ;; + '') ;; # Nothing to do for plain root method. + *) + echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'." + exit 1 + esac + echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'." else - SUDO= + if test "`id -u`" -ne "0" ; then + if command -v sudo 1>/dev/null 2>&1; then + SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=$0" + else + echo \"sudo\" is not available, will use \"su\" for installation steps... + SUDO=su_sudo + fi + else + SUDO= + fi fi ExperimentalBootstrap() { diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 2ac8d8d79..175325d40 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -84,39 +84,59 @@ fi # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using -# `su` +# `su`. Auto-detection can be overrided by explicitly setting the +# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below. + +# Because the parameters in `su -c` has to be a string, +# we need to properly escape it. +su_sudo() { + args="" + # This `while` loop iterates over all parameters given to this function. + # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string + # will be wrapped in a pair of `'`, then appended to `$args` string + # For example, `echo "It's only 1\$\!"` will be escaped to: + # 'echo' 'It'"'"'s only 1$!' + # │ │└┼┘│ + # │ │ │ └── `'s only 1$!'` the literal string + # │ │ └── `\"'\"` is a single quote (as a string) + # │ └── `'It'`, to be concatenated with the strings following it + # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself + while [ $# -ne 0 ]; do + args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " + shift + done + su root -c "$args" +} + SUDO_ENV="" export CERTBOT_AUTO="$0" -if test "`id -u`" -ne "0" ; then - if command -v sudo 1>/dev/null 2>&1; then - SUDO=sudo - SUDO_ENV="CERTBOT_AUTO=$0" - else - echo \"sudo\" is not available, will use \"su\" for installation steps... - # Because the parameters in `su -c` has to be a string, - # we need properly escape it - su_sudo() { - args="" - # This `while` loop iterates over all parameters given to this function. - # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string - # will be wrapped in a pair of `'`, then appended to `$args` string - # For example, `echo "It's only 1\$\!"` will be escaped to: - # 'echo' 'It'"'"'s only 1$!' - # │ │└┼┘│ - # │ │ │ └── `'s only 1$!'` the literal string - # │ │ └── `\"'\"` is a single quote (as a string) - # │ └── `'It'`, to be concatenated with the strings following it - # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself - while [ $# -ne 0 ]; do - args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " - shift - done - su root -c "$args" - } - SUDO=su_sudo - fi +if [ -n "${LE_AUTO_SUDO:+x}" ]; then + case "$LE_AUTO_SUDO" in + su_sudo|su) + SUDO=su_sudo + ;; + sudo) + SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=$0" + ;; + '') ;; # Nothing to do for plain root method. + *) + echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'." + exit 1 + esac + echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'." else - SUDO= + if test "`id -u`" -ne "0" ; then + if command -v sudo 1>/dev/null 2>&1; then + SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=$0" + else + echo \"sudo\" is not available, will use \"su\" for installation steps... + SUDO=su_sudo + fi + else + SUDO= + fi fi ExperimentalBootstrap() { From fd95a5505454f6f5983b189c63d7d554784532b2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 4 Nov 2016 18:39:58 -0700 Subject: [PATCH 244/331] use terminate not kill (#3750) --- certbot/plugins/manual.py | 3 +-- certbot/plugins/manual_test.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 48aec6e35..c124ce048 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -3,7 +3,6 @@ import os import logging import pipes import shutil -import signal import socket import subprocess import sys @@ -233,7 +232,7 @@ s.serve_forever()" """ "cleanup() must be called after perform()") if self._httpd.poll() is None: logger.debug("Terminating manual command process") - os.killpg(self._httpd.pid, signal.SIGTERM) + self._httpd.terminate() else: logger.debug("Manual command process already terminated " "with %s code", self._httpd.returncode) diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index 25107e4b4..828281951 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -1,5 +1,4 @@ """Tests for certbot.plugins.manual.""" -import signal import unittest import mock @@ -123,13 +122,12 @@ class AuthenticatorTest(unittest.TestCase): httpd.poll.return_value = 0 self.auth_test_mode.cleanup(self.achalls) - @mock.patch("certbot.plugins.manual.os.killpg", autospec=True) - def test_cleanup_test_mode_kills_still_running(self, mock_killpg): + def test_cleanup_test_mode_kills_still_running(self): # pylint: disable=protected-access self.auth_test_mode._httpd = httpd = mock.Mock(pid=1234) httpd.poll.return_value = None self.auth_test_mode.cleanup(self.achalls) - mock_killpg.assert_called_once_with(1234, signal.SIGTERM) + httpd.terminate.assert_called_once_with() if __name__ == "__main__": From f0ebd13ec293938848bb510fbcfc48dea1629eed Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 4 Nov 2016 19:03:00 -0700 Subject: [PATCH 245/331] [certbot-auto] Bump cryptography version to 1.5.2 (#3733) --- letsencrypt-auto-source/letsencrypt-auto | 51 ++++++++++--------- .../pieces/letsencrypt-auto-requirements.txt | 51 ++++++++++--------- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index f021140ce..73b1b4ac8 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -608,6 +608,11 @@ if [ "$1" = "--le-auto-phase2" ]; then # `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`, # and then use `hashin` or a more secure method to gather the hashes. +# Hashin example: +# pip install hashin +# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2 +# sets the new certbot-auto pinned version of cryptography to 1.5.2 + argparse==1.4.0 \ --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ --hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4 @@ -638,29 +643,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.3.4 \ - --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \ - --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \ - --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \ - --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \ - --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \ - --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \ - --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \ - --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \ - --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \ - --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \ - --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \ - --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \ - --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \ - --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \ - --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \ - --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \ - --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \ - --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \ - --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \ - --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \ - --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \ - --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9 +cryptography==1.5.2 \ + --hash=sha256:9e65f4c0ddcd4a7da3cfc1d87a0c3cf735a859c78f5f11d2346f7dfbc31df51b \ + --hash=sha256:a7e4a0f46ad767d4083faf31f4304301437f3919017203260620efbfeb72792b \ + --hash=sha256:0445831e72e38719e59b1f67b3361a0b43a52cb73ed10be757f6855310c75cb0 \ + --hash=sha256:4871e41cb3345c26b4596739a10c2dafa0a1207cef14ac9cdb923a55d0aa418b \ + --hash=sha256:eefe2e7f31833569d3ac9a4b796298f8d6deb0211c9c89e9f9ae7c774a6e982f \ + --hash=sha256:181f4f9943d95c144aad19264b31a3b7fa3a88a6d6a905fdfdfe985bf1ea0745 \ + --hash=sha256:3c73d538cb494924929a61376ecd7f802b28b12139546b9775d66ce8071efaf9 \ + --hash=sha256:713a68355550423dfb9167ed2365c1ad3aab1644cd7dcaf42afecc1e1d460dc5 \ + --hash=sha256:bf90be94e599ab3097f261ca606bad8c75b54eaeaaacfd0818fe6f2616ef9521 \ + --hash=sha256:446e9a139c87c09a07c9b252a2336bedffe99821f37332d4fc820d3da90d1738 \ + --hash=sha256:f04c00e81d42ec86e0b31a1d91783c3666691a85239f6daecfcda2cbe6c15f28 \ + --hash=sha256:5a7b9d10ef04e9cbbf0d9ba6a3502a1cc9176510e89522575d3e338f188eccfe \ + --hash=sha256:f2c35caf388e1f503b10c78ced239441f383c2b3d96e1b1c6fe04d56335af84e \ + --hash=sha256:f2b89bca3cd0efc7feb9aad0c8662508b4de3f26118808881868395ae32be337 \ + --hash=sha256:0194794e6bfc87a0fdb6e197a80ac5ea676e71bc2824281f9ccefb2be56ca2fb \ + --hash=sha256:44a968a20951481c9d8ffec4ba55326aa6d903c065568d0523600179ffd3976d \ + --hash=sha256:cd9a1734b0818ff57728affeb2444a2465a006e8a1eeee9d59ee4b5727a67ee7 \ + --hash=sha256:eb7f016eca0188bd310e2bd05b379ba378cd83b8962899e58f0f91feca607e47 \ + --hash=sha256:050caca01c8a67b4f1b10f2eca085c00154d1553a983d4ff7dfe2add9a270eaa \ + --hash=sha256:ef5692b5e44587e92b1231154bec5c7d0a262c75c4be754a6e605de8614145c6 \ + --hash=sha256:b634baf73c2b2f0e9c338434531aca3adffef47e78cba909da0ddcc9448f1c84 \ + --hash=sha256:eb8875736734e8e870b09be43b17f40472dc189b1c422a952fa8580768204832 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index de83178b0..34f272e96 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -3,6 +3,11 @@ # `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`, # and then use `hashin` or a more secure method to gather the hashes. +# Hashin example: +# pip install hashin +# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2 +# sets the new certbot-auto pinned version of cryptography to 1.5.2 + argparse==1.4.0 \ --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ --hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4 @@ -33,29 +38,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.3.4 \ - --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \ - --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \ - --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \ - --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \ - --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \ - --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \ - --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \ - --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \ - --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \ - --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \ - --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \ - --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \ - --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \ - --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \ - --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \ - --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \ - --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \ - --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \ - --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \ - --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \ - --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \ - --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9 +cryptography==1.5.2 \ + --hash=sha256:9e65f4c0ddcd4a7da3cfc1d87a0c3cf735a859c78f5f11d2346f7dfbc31df51b \ + --hash=sha256:a7e4a0f46ad767d4083faf31f4304301437f3919017203260620efbfeb72792b \ + --hash=sha256:0445831e72e38719e59b1f67b3361a0b43a52cb73ed10be757f6855310c75cb0 \ + --hash=sha256:4871e41cb3345c26b4596739a10c2dafa0a1207cef14ac9cdb923a55d0aa418b \ + --hash=sha256:eefe2e7f31833569d3ac9a4b796298f8d6deb0211c9c89e9f9ae7c774a6e982f \ + --hash=sha256:181f4f9943d95c144aad19264b31a3b7fa3a88a6d6a905fdfdfe985bf1ea0745 \ + --hash=sha256:3c73d538cb494924929a61376ecd7f802b28b12139546b9775d66ce8071efaf9 \ + --hash=sha256:713a68355550423dfb9167ed2365c1ad3aab1644cd7dcaf42afecc1e1d460dc5 \ + --hash=sha256:bf90be94e599ab3097f261ca606bad8c75b54eaeaaacfd0818fe6f2616ef9521 \ + --hash=sha256:446e9a139c87c09a07c9b252a2336bedffe99821f37332d4fc820d3da90d1738 \ + --hash=sha256:f04c00e81d42ec86e0b31a1d91783c3666691a85239f6daecfcda2cbe6c15f28 \ + --hash=sha256:5a7b9d10ef04e9cbbf0d9ba6a3502a1cc9176510e89522575d3e338f188eccfe \ + --hash=sha256:f2c35caf388e1f503b10c78ced239441f383c2b3d96e1b1c6fe04d56335af84e \ + --hash=sha256:f2b89bca3cd0efc7feb9aad0c8662508b4de3f26118808881868395ae32be337 \ + --hash=sha256:0194794e6bfc87a0fdb6e197a80ac5ea676e71bc2824281f9ccefb2be56ca2fb \ + --hash=sha256:44a968a20951481c9d8ffec4ba55326aa6d903c065568d0523600179ffd3976d \ + --hash=sha256:cd9a1734b0818ff57728affeb2444a2465a006e8a1eeee9d59ee4b5727a67ee7 \ + --hash=sha256:eb7f016eca0188bd310e2bd05b379ba378cd83b8962899e58f0f91feca607e47 \ + --hash=sha256:050caca01c8a67b4f1b10f2eca085c00154d1553a983d4ff7dfe2add9a270eaa \ + --hash=sha256:ef5692b5e44587e92b1231154bec5c7d0a262c75c4be754a6e605de8614145c6 \ + --hash=sha256:b634baf73c2b2f0e9c338434531aca3adffef47e78cba909da0ddcc9448f1c84 \ + --hash=sha256:eb8875736734e8e870b09be43b17f40472dc189b1c422a952fa8580768204832 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 From 549f0eccf90d71a5a0bc93ce1a01281ad10ace05 Mon Sep 17 00:00:00 2001 From: Nick Fong Date: Mon, 7 Nov 2016 11:36:58 -0800 Subject: [PATCH 246/331] Remove get_all_certs_keys() from interfaces.py (#3753) - Remove method 'get_all_certs_keys()' from interfaces.py - Also remove 'get_all_certs_keys()' from plugins/null.py and corresponding unit test --- certbot/interfaces.py | 13 ------------- certbot/plugins/null.py | 3 --- certbot/plugins/null_test.py | 1 - 3 files changed, 17 deletions(-) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 5872c38d8..337bb3238 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -300,19 +300,6 @@ class IInstaller(IPlugin): """ - def get_all_certs_keys(): - """Retrieve all certs and keys set in configuration. - - :returns: tuples with form `[(cert, key, path)]`, where: - - - `cert` - str path to certificate file - - `key` - str path to associated key file - - `path` - file path to configuration file - - :rtype: list - - """ - def save(title=None, temporary=False): """Saves all changes to the configuration files. diff --git a/certbot/plugins/null.py b/certbot/plugins/null.py index 995b96274..87c0737a5 100644 --- a/certbot/plugins/null.py +++ b/certbot/plugins/null.py @@ -40,9 +40,6 @@ class Installer(common.Plugin): def supported_enhancements(self): return [] - def get_all_certs_keys(self): - return [] - def save(self, title=None, temporary=False): pass # pragma: no cover diff --git a/certbot/plugins/null_test.py b/certbot/plugins/null_test.py index 305954a2f..0d04a2bc5 100644 --- a/certbot/plugins/null_test.py +++ b/certbot/plugins/null_test.py @@ -15,7 +15,6 @@ class InstallerTest(unittest.TestCase): self.assertTrue(isinstance(self.installer.more_info(), str)) self.assertEqual([], self.installer.get_all_names()) self.assertEqual([], self.installer.supported_enhancements()) - self.assertEqual([], self.installer.get_all_certs_keys()) if __name__ == "__main__": From 82c69815d11a0c947b16be05ff1208363a09b110 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Mon, 7 Nov 2016 23:22:20 +0100 Subject: [PATCH 247/331] Fix writing pem files with Python3 (#3757) * Standardize arguments name for mode and chmod in the util API * Handle OpenSSL pem as bytes objects only for Python3 compatibility * Handle OpenSSL pem as bytes objects only (remaining bits) * Manipulate bytes objects only when testing PEM-related functions * Fix argument order when calling util.unique_file --- certbot-nginx/certbot_nginx/configurator.py | 3 ++- certbot/client.py | 6 +++--- certbot/crypto_util.py | 9 +++++---- certbot/storage.py | 8 ++++---- certbot/tests/crypto_util_test.py | 8 ++++---- certbot/tests/storage_test.py | 18 +++++++++--------- certbot/util.py | 20 +++++++++++--------- 7 files changed, 38 insertions(+), 34 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index bfc7b6a67..503599423 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -405,7 +405,8 @@ class NginxConfigurator(common.Plugin): cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()]) cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) - cert_file, cert_path = util.unique_file(os.path.join(tmp_dir, "cert.pem")) + cert_file, cert_path = util.unique_file( + os.path.join(tmp_dir, "cert.pem"), mode="wb") with cert_file: cert_file.write(cert_pem) return cert_path, le_key.file diff --git a/certbot/client.py b/certbot/client.py index 55f3d5e67..880cfe7df 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -322,7 +322,7 @@ class Client(object): self.config.strict_permissions) cert_pem = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode('ascii') + OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path) @@ -595,10 +595,10 @@ def _open_pem_file(cli_arg_path, pem_path): """ if cli.set_by_cli(cli_arg_path): - return util.safe_open(pem_path, chmod=0o644),\ + return util.safe_open(pem_path, chmod=0o644, mode="wb"),\ os.path.abspath(pem_path) else: - uniq = util.unique_file(pem_path, 0o644) + uniq = util.unique_file(pem_path, 0o644, "wb") return uniq[0], os.path.abspath(uniq[1]) def _save_chain(chain_pem, chain_file): diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 7253742b0..65e3de345 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -53,7 +53,8 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): # Save file util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), config.strict_permissions) - key_f, key_path = util.unique_file(os.path.join(key_dir, keyname), 0o600) + key_f, key_path = util.unique_file( + os.path.join(key_dir, keyname), 0o600, "wb") with key_f: key_f.write(key_pem) @@ -85,7 +86,7 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"): util.make_or_verify_dir(path, 0o755, os.geteuid(), config.strict_permissions) csr_f, csr_filename = util.unique_file( - os.path.join(path, csrname), 0o644) + os.path.join(path, csrname), 0o644, "wb") csr_f.write(csr_pem) csr_f.close() @@ -351,11 +352,11 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): if isinstance(cert, jose.ComparableX509): # pylint: disable=protected-access cert = cert.wrapped - return OpenSSL.crypto.dump_certificate(filetype, cert).decode('ascii') + return OpenSSL.crypto.dump_certificate(filetype, cert) # assumes that OpenSSL.crypto.dump_certificate includes ending # newline character - return "".join(_dump_cert(cert) for cert in chain) + return b"".join(_dump_cert(cert) for cert in chain) def notBefore(cert_path): diff --git a/certbot/storage.py b/certbot/storage.py index 2134cd90b..5d7eeb88a 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -817,17 +817,17 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes for kind in ALL_FOUR: os.symlink(os.path.join(archive, kind + "1.pem"), target[kind]) - with open(target["cert"], "w") as f: + with open(target["cert"], "wb") as f: logger.debug("Writing certificate to %s.", target["cert"]) f.write(cert) - with open(target["privkey"], "w") as f: + with open(target["privkey"], "wb") as f: logger.debug("Writing private key to %s.", target["privkey"]) f.write(privkey) # XXX: Let's make sure to get the file permissions right here - with open(target["chain"], "w") as f: + with open(target["chain"], "wb") as f: logger.debug("Writing chain to %s.", target["chain"]) f.write(chain) - with open(target["fullchain"], "w") as f: + with open(target["fullchain"], "wb") as f: # assumes that OpenSSL.crypto.dump_certificate includes # ending newline character logger.debug("Writing full chain to %s.", target["fullchain"]) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index c0dc1de3a..4832e2869 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -40,9 +40,9 @@ class InitSaveKeyTest(unittest.TestCase): @mock.patch('certbot.crypto_util.make_key') def test_success(self, mock_make): - mock_make.return_value = 'key_pem' + mock_make.return_value = b'key_pem' key = self._call(1024, self.key_dir) - self.assertEqual(key.pem, 'key_pem') + self.assertEqual(key.pem, b'key_pem') self.assertTrue('key-certbot.pem' in key.file) @mock.patch('certbot.crypto_util.make_key') @@ -67,13 +67,13 @@ class InitSaveCSRTest(unittest.TestCase): def test_it(self, unused_mock_verify, mock_csr): from certbot.crypto_util import init_save_csr - mock_csr.return_value = ('csr_pem', 'csr_der') + mock_csr.return_value = (b'csr_pem', b'csr_der') csr = init_save_csr( mock.Mock(pem='dummy_key'), 'example.com', self.csr_dir, 'csr-certbot.pem') - self.assertEqual(csr.data, 'csr_der') + self.assertEqual(csr.data, b'csr_der') self.assertTrue('csr-certbot.pem' in csr.file) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index bfbcd885e..fb33a1864 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -573,18 +573,18 @@ class RenewableCertTests(BaseRenewableCertTest): from certbot import storage result = storage.RenewableCert.new_lineage( - "the-lineage.com", "cert", "privkey", "chain", self.cli_config) + "the-lineage.com", b"cert", b"privkey", b"chain", self.cli_config) # This consistency check tests most relevant properties about the # newly created cert lineage. # pylint: disable=protected-access self.assertTrue(result._consistent()) self.assertTrue(os.path.exists(os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) - with open(result.fullchain) as f: - self.assertEqual(f.read(), "cert" + "chain") + with open(result.fullchain, "rb") as f: + self.assertEqual(f.read(), b"cert" + b"chain") # Let's do it again and make sure it makes a different lineage result = storage.RenewableCert.new_lineage( - "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config) self.assertTrue(os.path.exists(os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) # Now trigger the detection of already existing files @@ -592,15 +592,15 @@ class RenewableCertTests(BaseRenewableCertTest): self.cli_config.live_dir, "the-lineage.com-0002")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "the-lineage.com", - "cert3", "privkey3", "chain3", self.cli_config) + b"cert3", b"privkey3", b"chain3", self.cli_config) os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, - "other-example.com", "cert4", - "privkey4", "chain4", self.cli_config) + "other-example.com", b"cert4", + b"privkey4", b"chain4", self.cli_config) # Make sure it can accept renewal parameters result = storage.RenewableCert.new_lineage( - "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config) # TODO: Conceivably we could test that the renewal parameters actually # got saved @@ -617,7 +617,7 @@ class RenewableCertTests(BaseRenewableCertTest): shutil.rmtree(self.cli_config.live_dir) storage.RenewableCert.new_lineage( - "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) + "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config) self.assertTrue(os.path.exists( os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) diff --git a/certbot/util.py b/certbot/util.py index f3a74d47d..220795237 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -151,11 +151,11 @@ def safe_open(path, mode="w", chmod=None, buffering=None): mode, *fdopen_args) -def _unique_file(path, filename_pat, count, mode): +def _unique_file(path, filename_pat, count, chmod, mode): while True: current_path = os.path.join(path, filename_pat(count)) try: - return safe_open(current_path, chmod=mode),\ + return safe_open(current_path, chmod=chmod, mode=mode),\ os.path.abspath(current_path) except OSError as err: # "File exists," is okay, try a different name. @@ -164,11 +164,12 @@ def _unique_file(path, filename_pat, count, mode): count += 1 -def unique_file(path, mode=0o777): +def unique_file(path, chmod=0o777, mode="w"): """Safely finds a unique file. :param str path: path/filename.ext - :param int mode: File mode + :param int chmod: File mode + :param str mode: Open mode :returns: tuple of file object and file name @@ -176,15 +177,16 @@ def unique_file(path, mode=0o777): path, tail = os.path.split(path) return _unique_file( path, filename_pat=(lambda count: "%04d_%s" % (count, tail)), - count=0, mode=mode) + count=0, chmod=chmod, mode=mode) -def unique_lineage_name(path, filename, mode=0o777): +def unique_lineage_name(path, filename, chmod=0o777, mode="w"): """Safely finds a unique file using lineage convention. :param str path: directory path :param str filename: proposed filename - :param int mode: file mode + :param int chmod: file mode + :param str mode: open mode :returns: tuple of file object and file name (which may be modified from the requested one by appending digits to ensure uniqueness) @@ -196,13 +198,13 @@ def unique_lineage_name(path, filename, mode=0o777): """ preferred_path = os.path.join(path, "%s.conf" % (filename)) try: - return safe_open(preferred_path, chmod=mode), preferred_path + return safe_open(preferred_path, chmod=chmod), preferred_path except OSError as err: if err.errno != errno.EEXIST: raise return _unique_file( path, filename_pat=(lambda count: "%s-%04d.conf" % (filename, count)), - count=1, mode=mode) + count=1, chmod=chmod, mode=mode) def safely_remove(path): From d197b5aa0568eb3952e85df7cc0126e1ae50a372 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 7 Nov 2016 14:53:15 -0800 Subject: [PATCH 248/331] Fix OS Documentation (#3747) * Update various package names in using.rst from "letsencrypt" to "certbot" * Update using.rst Change package name --- docs/install.rst | 8 ++++---- docs/using.rst | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index 182abdb71..910d23149 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -153,13 +153,13 @@ repo, if you have not already done so. Then run: .. code-block:: shell - sudo apt-get install letsencrypt python-letsencrypt-apache -t jessie-backports + sudo apt-get install certbot python-certbot-apache -t jessie-backports **Fedora** .. code-block:: shell - sudo dnf install letsencrypt + sudo dnf install certbot python2-certbot-apache **Gentoo** @@ -168,8 +168,8 @@ want to use the Apache plugin, it has to be installed separately: .. code-block:: shell - emerge -av app-crypt/letsencrypt - emerge -av app-crypt/letsencrypt-apache + emerge -av app-crypt/certbot + emerge -av app-crypt/certbot-apache When using the Apache plugin, you will run into a "cannot find a cert or key directive" error if you're sporting the default Gentoo ``httpd.conf``. diff --git a/docs/using.rst b/docs/using.rst index 1becea8ea..49f30ff58 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -461,5 +461,3 @@ give us as much information as possible: - copy and paste ``certbot --version`` output - your operating system, including specific version - specify which installation method you've chosen - - From d741e684d02202c76da999d03cee7af5b166fc99 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 8 Nov 2016 01:22:48 +0200 Subject: [PATCH 249/331] Script plugin (#3521) * Script plugin initial commit * Fix auth script path * Return correct responses * Added DNS-01 support * Report the challenge pref correctly * Use config root from certbot constants rather than hardcoded * Remove prehook and rename posthook to cleanup for clarity * Refactoring * Docs * Refactoring * Refactoring continued, working now * Use global preferred-challenges argument in favor of local * Added http-01 as fallback challenge if not defined * Do not continue if auth script not defined * Skip unnecessary steps when running * Read config values from correct places * Tests and minor fixes * Make Python 2.6 happy again * Added CERTBOT_AUTH_OUTPUT and better tests * Lint & Py3 fixes * Make Python 2.6 happy again * Doc changes * Refactor hook execute and reuse in script plugin * Refactored hook validation * Added long_description for plugin help text * Refactored env var writing --- certbot/cli.py | 8 +- certbot/hooks.py | 27 ++++-- certbot/plugins/disco.py | 8 ++ certbot/plugins/disco_test.py | 12 +++ certbot/plugins/script.py | 161 +++++++++++++++++++++++++++++++ certbot/plugins/script_test.py | 170 +++++++++++++++++++++++++++++++++ certbot/plugins/selection.py | 4 +- setup.py | 1 + 8 files changed, 381 insertions(+), 10 deletions(-) create mode 100644 certbot/plugins/script.py create mode 100644 certbot/plugins/script_test.py diff --git a/certbot/cli.py b/certbot/cli.py index c27a278f3..41afa1391 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -80,6 +80,7 @@ USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing c --standalone Run a standalone webserver for authentication %s --webroot Place files in a server's webroot folder for authentication + --script User provided shell scripts for authentication OR use different plugins to obtain (authenticate) the cert and then install it: @@ -92,7 +93,7 @@ More detailed help: all, automation, paths, security, testing, or any of the subcommands or plugins (certonly, renew, install, register, nginx, apache, standalone, - webroot, etc.) + webroot, script, etc.) """ @@ -587,7 +588,8 @@ class HelpfulArgumentParser(object): """ for name, plugin_ep in six.iteritems(plugins): - parser_or_group = self.add_group(name, description=plugin_ep.description) + parser_or_group = self.add_group(name, + description=plugin_ep.long_description) plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name) def determine_help_topics(self, chosen_topic): @@ -989,6 +991,8 @@ def _plugins_parsing(helpful, plugins): help="Obtain and install certs using Nginx") helpful.add(["plugins", "certonly"], "--standalone", action="store_true", help='Obtain certs using a "standalone" webserver.') + helpful.add(["plugins", "certonly"], "--script", action="store_true", + help='Obtain certs using shell script(s)') helpful.add(["plugins", "certonly"], "--manual", action="store_true", help='Provide laborious manual instructions for obtaining a cert') helpful.add(["plugins", "certonly"], "--webroot", action="store_true", diff --git a/certbot/hooks.py b/certbot/hooks.py index f37f81c6e..37afee9b0 100644 --- a/certbot/hooks.py +++ b/certbot/hooks.py @@ -12,16 +12,16 @@ logger = logging.getLogger(__name__) def validate_hooks(config): """Check hook commands are executable.""" - _validate_hook(config.pre_hook, "pre") - _validate_hook(config.post_hook, "post") - _validate_hook(config.renew_hook, "renew") + validate_hook(config.pre_hook, "pre") + validate_hook(config.post_hook, "post") + validate_hook(config.renew_hook, "renew") def _prog(shell_cmd): """Extract the program run by a shell command""" cmd = _which(shell_cmd) return os.path.basename(cmd) if cmd else None -def _validate_hook(shell_cmd, hook_name): +def validate_hook(shell_cmd, hook_name): """Check that a command provided as a hook is plausibly executable. :raises .errors.HookCommandNotFound: if the command is not found @@ -69,17 +69,30 @@ def renew_hook(config, domains, lineage_path): else: logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook) + def _run_hook(shell_cmd): """Run a hook command. :returns: stderr if there was any""" - cmd = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE) - _out, err = cmd.communicate() + err, _ = execute(shell_cmd) + return err + + +def execute(shell_cmd): + """Run a command. + + :returns: `tuple` (`str` stderr, `str` stdout)""" + + cmd = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE) + out, err = cmd.communicate() if cmd.returncode != 0: - logger.error('Hook command "%s" returned error code %d', shell_cmd, cmd.returncode) + logger.error('Hook command "%s" returned error code %d', + shell_cmd, cmd.returncode) if err: logger.error('Error output from %s:\n%s', _prog(shell_cmd), err) + return (err, out) + def _is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index a6e8e7ed7..ba532eb1b 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -53,6 +53,14 @@ class PluginEntryPoint(object): """Description with name. Handy for UI.""" return "{0} ({1})".format(self.description, self.name) + @property + def long_description(self): + """Long description of the plugin.""" + try: + return self.plugin_cls.long_description + except AttributeError: + return self.description + @property def hidden(self): """Should this plugin be hidden from UI?""" diff --git a/certbot/plugins/disco_test.py b/certbot/plugins/disco_test.py index dadcde37d..7282c9ec8 100644 --- a/certbot/plugins/disco_test.py +++ b/certbot/plugins/disco_test.py @@ -63,6 +63,18 @@ class PluginEntryPointTest(unittest.TestCase): self.assertEqual( "Desc (sa)", self.plugin_ep.description_with_name) + def test_long_description(self): + self.plugin_ep.plugin_cls = mock.MagicMock( + long_description="Long desc") + self.assertEqual( + "Long desc", self.plugin_ep.long_description) + + def test_long_description_nonexistent(self): + self.plugin_ep.plugin_cls = mock.MagicMock( + description="Long desc not found", spec=["description"]) + self.assertEqual( + "Long desc not found", self.plugin_ep.long_description) + def test_ifaces(self): self.assertTrue(self.plugin_ep.ifaces((interfaces.IAuthenticator,))) self.assertFalse(self.plugin_ep.ifaces((interfaces.IInstaller,))) diff --git a/certbot/plugins/script.py b/certbot/plugins/script.py new file mode 100644 index 000000000..049ee8c96 --- /dev/null +++ b/certbot/plugins/script.py @@ -0,0 +1,161 @@ +"""Script-based Authenticator.""" +import logging +import os +import sys + +import zope.interface + +from acme import challenges + +from certbot import errors +from certbot import interfaces +from certbot import hooks + +from certbot.plugins import common + +logger = logging.getLogger(__name__) + + +CHALLENGES = ["http-01", "dns-01"] + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(common.Plugin): + """Script authenticator + + calls user defined script to perform authentication and + optionally cleanup. + + """ + + description = "Authenticate using user provided script(s)" + + long_description = ("Authenticate using user provided script(s). " + + "Authenticator script has the following environment " + + "variables available for it: " + + "CERTBOT_DOMAIN - The domain being authenticated " + + "CERTBOT_VALIDATION - The validation string " + + "CERTBOT_TOKEN - Resource name part of HTTP-01 " + + "challenge (HTTP-01 only). " + + "Cleanup script has all the above, and additional " + + "var: CERTBOT_AUTH_OUTPUT - stdout output from the " + + "authenticator" + ) + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.cleanup_script = None + self.auth_script = None + self.challenges = [] + + @classmethod + def add_parser_arguments(cls, add): + add("auth", default=None, required=False, + help="path or command for the authentication script") + add("cleanup", default=None, required=False, + help="path or command for the cleanup script") + + @property + def supported_challenges(self): + """Challenges supported by this plugin.""" + return self.challenges + + def more_info(self): # pylint: disable=missing-docstring + return("This authenticator enables user to perform authentication " + + "using shell script(s).") + + def prepare(self): + """Prepare script plugin, check challenge, scripts and register them""" + pref_challenges = self.config.pref_challs + for c in pref_challenges: + if c.typ in CHALLENGES: + self.challenges.append(c) + if not self.challenges and len(pref_challenges): + # Challenges requested, but not supported + raise errors.PluginError( + "Unfortunately script plugin doesn't yet support " + + "the requested challenges") + + # Challenge not defined on cli, set default + if not self.challenges: + self.challenges.append(challenges.Challenge.TYPES["http-01"]) + + if not self.conf("auth"): + raise errors.PluginError("Parameter --script-auth is required " + + "for script plugin") + self._prepare_scripts() + + def _prepare_scripts(self): + """Helper method for prepare, to take care of validating scripts""" + script_path = self.conf("auth") + cleanup_path = self.conf("cleanup") + if self.config.validate_hooks: + hooks.validate_hook(script_path, "script_auth") + self.auth_script = script_path + if cleanup_path: + if self.config.validate_hooks: + hooks.validate_hook(cleanup_path, "script_cleanup") + self.cleanup_script = cleanup_path + + def get_chall_pref(self, domain): + """Return challenge(s) we're answering to """ + # pylint: disable=unused-argument + return self.challenges + + def perform(self, achalls): + """Perform the authentication per challenge""" + mapping = {"http-01": self._setup_env_http, + "dns-01": self._setup_env_dns} + responses = [] + for achall in achalls: + response, validation = achall.response_and_validation() + # Setup env vars + mapping[achall.typ](achall, validation) + output = self.execute(self.auth_script) + if output: + self._write_auth_output(output) + responses.append(response) + return responses + + def _setup_env_http(self, achall, validation): + """Write environment variables for http challenge""" + ev = dict() + ev["CERTBOT_TOKEN"] = achall.chall.encode("token") + ev["CERTBOT_VALIDATION"] = validation + ev["CERTBOT_DOMAIN"] = achall.domain + os.environ.update(ev) + + def _setup_env_dns(self, achall, validation): + """Write environment variables for dns challenge""" + ev = dict() + ev["CERTBOT_VALIDATION"] = validation + ev["CERTBOT_DOMAIN"] = achall.domain + os.environ.update(ev) + + def _write_auth_output(self, out): + """Write output from auth script to env var for + cleanup to act upon""" + os.environ.update({"CERTBOT_AUTH_OUTPUT": out.strip()}) + + def _normalize_string(self, value): + """Return string instead of bytestring for Python3. + Helper function for writing env vars, as os.environ needs str""" + + if isinstance(value, bytes): + value = value.decode(sys.getdefaultencoding()) + return str(value) + + def execute(self, shell_cmd): + """Run a script. + + :param str shell_cmd: Command to run + :returns: `str` stdout output""" + + _, out = hooks.execute(shell_cmd) + return self._normalize_string(out) + + def cleanup(self, achalls): # pylint: disable=unused-argument + """Run cleanup.sh """ + if self.cleanup_script: + self.execute(self.cleanup_script) diff --git a/certbot/plugins/script_test.py b/certbot/plugins/script_test.py new file mode 100644 index 000000000..0c13d84db --- /dev/null +++ b/certbot/plugins/script_test.py @@ -0,0 +1,170 @@ +"""Tests for certbot.plugins.manual.""" +import os +import tempfile +import unittest + +import mock + +from acme import challenges +from acme import jose + +from certbot import achallenges +from certbot import errors + +from certbot.tests import acme_util +from certbot.tests import test_util + + +KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) + + +class AuthenticatorTest(unittest.TestCase): + """Tests for certbot.plugins.script.Authenticator.""" + + def setUp(self): + from certbot.plugins.script import Authenticator + self.auth_return_value = "return from auth\n" + self.script_nonexec = create_script(b'# empty') + self.script_exec = create_script_exec(b'echo "return from auth\n"') + self.config = mock.MagicMock( + script_auth=self.script_exec, + script_cleanup=self.script_exec, + pref_challs=[challenges.Challenge.TYPES["http-01"], + challenges.Challenge.TYPES["dns-01"], + challenges.Challenge.TYPES["tls-sni-01"]]) + + self.tlssni_config = mock.MagicMock( + script_auth=self.script_exec, + script_cleanup=self.script_exec, + pref_challs=[challenges.Challenge.TYPES["tls-sni-01"]]) + + self.nochall_config = mock.MagicMock( + script_auth=self.script_exec, + script_cleanup=self.script_exec, + ) + + self.default = Authenticator(config=self.config, name="script") + self.onlytlssni = Authenticator(config=self.tlssni_config, + name="script") + self.nochall = Authenticator(config=self.nochall_config, + name="script") + + self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY) + self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY) + + self.achalls = [self.http01, self.dns01] + + def tearDown(self): + os.remove(self.script_exec) + os.remove(self.script_nonexec) + + def test_prepare_normal(self): + """Test prepare with typical configuration""" + from certbot.plugins.script import Authenticator + # Erroring combinations in from of (auth_script, cleanup_script, error) + for v in [("/NONEXISTENT/script.sh", "/NONEXISTENT/script.sh", + errors.HookCommandNotFound), + (self.script_nonexec, "/NONEXISTENT/script.sh", + errors.HookCommandNotFound), + (self.script_exec, "/NONEXISTENT/script.sh", + errors.HookCommandNotFound), + ("/NONEXISTENT/script.sh", self.script_nonexec, + errors.HookCommandNotFound), + ("/NONEXISTENT/script.sh", self.script_exec, + errors.HookCommandNotFound), + (None, self.script_exec, + errors.PluginError)]: + testconf = mock.MagicMock( + script_auth=v[0], + script_cleanup=v[1], + pref_challs=[challenges.Challenge.TYPES["http-01"]]) + testauth = Authenticator(config=testconf, name="script") + self.assertRaises(v[2], testauth.prepare) + + # This should not error + self.default.prepare() + self.assertEqual(len(self.default.challenges), 2) + + def test_prepare_tlssni(self): + """Test for provided, but unsupported challenge type""" + self.assertRaises(errors.PluginError, self.onlytlssni.prepare) + + def test_prepare_nochall(self): + """Test for default challenge""" + self.nochall.prepare() + self.assertEqual(len(self.nochall.challenges), 1) + + def test_more_info(self): + self.assertTrue(isinstance(self.default.more_info(), str)) + + def test_get_chall_pref(self): + self.default.prepare() + self.assertTrue(all(issubclass(pref, challenges.Challenge) + for pref in self.default.get_chall_pref( + "foo.com"))) + + def test_get_supported_challenges(self): + self.default.prepare() + self.assertTrue(all(issubclass(sup, challenges.Challenge) + for sup in self.default.supported_challenges)) + + def test_perform(self): + resp_http = self.http01.response(KEY) + resp_dns = self.dns01.response(KEY) + self.default.prepare() + # Check for the env vars prior to the run + self.assertFalse("CERTBOT_VALIDATION" in os.environ.keys()) + self.assertFalse("CERTBOT_DOMAIN" in os.environ.keys()) + self.assertFalse("CERTBOT_AUTH_OUTPUT" in os.environ.keys()) + + pref_resp = self.default.perform(self.achalls) + self.assertEqual([resp_http, resp_dns], pref_resp) + # Check for the env vars post run + self.assertTrue("CERTBOT_VALIDATION" in os.environ.keys()) + self.assertTrue("CERTBOT_DOMAIN" in os.environ.keys()) + self.assertTrue("CERTBOT_AUTH_OUTPUT" in os.environ.keys()) + self.assertEqual(os.environ["CERTBOT_AUTH_OUTPUT"], + self.auth_return_value.strip()) + + @mock.patch('certbot.plugins.script.Authenticator.execute') + def test_cleanup(self, mock_exec): + mock_exec.return_value = (0, None, None) + self.default.prepare() + self.default.cleanup(self.achalls) + self.assertEqual(mock_exec.call_count, 1) + + @mock.patch('certbot.hooks.Popen') + def test_execute(self, mock_popen): + proc = mock.Mock() + # tuple values: stdout, stderr, errorcode, num_of_logger_calls + for t in [("", "", 0, 0), + (self.auth_return_value, "", 0, 0), + (None, "stderr_output", 0, 1), + ("whatever", "stderr_output", 1, 2), + (b'bytestring outval', "", 0, 0)]: + proc = mock.Mock() + attrs = {'communicate.return_value': (t[0], t[1]), + 'returncode': t[2]} + proc.configure_mock(**attrs) # pylint: disable=star-args + mock_popen.return_value = proc + with mock.patch('certbot.hooks.logger.error') as mock_log: + output = self.default.execute(self.script_exec) + self.assertEqual(mock_log.call_count, t[3]) + self.assertTrue(isinstance(output, str)) + + +def create_script(contents): + """ Helper to create temporary file """ + f = tempfile.NamedTemporaryFile(delete=False, prefix='.sh') + f.write(contents) + f.close() + return f.name + + +def create_script_exec(contents): + """ Helper to create temporary file with exec permissions""" + fname = create_script(contents) + os.chmod(fname, 0o700) + return fname diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 8f371f586..ed0991a89 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -131,7 +131,7 @@ def choose_plugin(prepared, question): else: return None -noninstaller_plugins = ["webroot", "manual", "standalone"] +noninstaller_plugins = ["webroot", "manual", "standalone", "script"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -236,6 +236,8 @@ def cli_plugin_requests(config): req_auth = set_configurator(req_auth, "webroot") if config.manual: req_auth = set_configurator(req_auth, "manual") + if config.script: + req_auth = set_configurator(req_auth, "script") logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) return req_auth, req_inst diff --git a/setup.py b/setup.py index f2d021c97..90c98d469 100644 --- a/setup.py +++ b/setup.py @@ -132,6 +132,7 @@ setup( 'null = certbot.plugins.null:Installer', 'standalone = certbot.plugins.standalone:Authenticator', 'webroot = certbot.plugins.webroot:Authenticator', + 'script = certbot.plugins.script:Authenticator', ], }, ) From 0bc3e1860bec562456cdad727d58b30b9b02937a Mon Sep 17 00:00:00 2001 From: Gilles Pietri Date: Tue, 8 Nov 2016 00:31:50 +0100 Subject: [PATCH 250/331] Add renew_hook to options stored in the renewal config, partially tackles #3394 (#3724) --- certbot/renewal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/renewal.py b/certbot/renewal.py index 5e57c3e20..f5b2efa46 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) # the renewal configuration process loses this information. STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent", "server", "account", "authenticator", "installer", - "standalone_supported_challenges"] + "standalone_supported_challenges", "renew_hook"] INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"] From df10a6431bd98609775cded7f72410dab07bf438 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Mon, 7 Nov 2016 15:48:46 -0800 Subject: [PATCH 251/331] Don't re-add redirects if one exists (#3751) * Don't re-add redirects if one exists * coverage * make coverage happy * don't re-add comment, and clean code --- certbot-nginx/certbot_nginx/configurator.py | 72 +++++++++++++++--- certbot-nginx/certbot_nginx/obj.py | 30 ++++++++ certbot-nginx/certbot_nginx/parser.py | 1 - .../certbot_nginx/tests/configurator_test.py | 58 ++++++++++++++ certbot-nginx/certbot_nginx/tests/obj_test.py | 76 ++++++++++++++++++- 5 files changed, 224 insertions(+), 13 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 503599423..94ac7de86 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -29,6 +29,36 @@ from certbot_nginx import parser logger = logging.getLogger(__name__) +REDIRECT_BLOCK = [[ + ['\n ', 'if', ' ', '($scheme != "https") '], + [['\n ', 'return', ' ', '301 https://$host$request_uri'], + '\n '] +], ['\n']] + +TEST_REDIRECT_BLOCK = [ + [ + ['if', '($scheme != "https")'], + [ + ['return', '301 https://$host$request_uri'] + ] + ], + ['#', ' managed by Certbot'] +] + +REDIRECT_COMMENT_BLOCK = [ + ['\n ', '#', ' Redirect non-https traffic to https'], + ['\n ', '#', ' if ($scheme != "https") {'], + ['\n ', '#', " return 301 https://$host$request_uri;"], + ['\n ', '#', " } # managed by Certbot"], + ['\n'] +] + +TEST_REDIRECT_COMMENT_BLOCK = [ + ['#', ' Redirect non-https traffic to https'], + ['#', ' if ($scheme != "https") {'], + ['#', " return 301 https://$host$request_uri;"], + ['#', " } # managed by Certbot"], +] @zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller) @zope.interface.provider(interfaces.IPluginFactory) @@ -483,6 +513,23 @@ class NginxConfigurator(common.Plugin): logger.warning("Failed %s for %s", enhancement, domain) raise + def _has_certbot_redirect(self, vhost): + return vhost.contains_list(TEST_REDIRECT_BLOCK) + + def _has_certbot_redirect_comment(self, vhost): + return vhost.contains_list(TEST_REDIRECT_COMMENT_BLOCK) + + def _add_redirect_block(self, vhost, active=True): + """Add redirect directive to vhost + """ + if active: + redirect_block = REDIRECT_BLOCK + else: + redirect_block = REDIRECT_COMMENT_BLOCK + + self.parser.add_server_directives( + vhost, redirect_block, replace=False) + def _enable_redirect(self, domain, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. @@ -505,17 +552,20 @@ class NginxConfigurator(common.Plugin): logger.info("No matching insecure server blocks listening on port %s found.", self.DEFAULT_LISTEN_PORT) else: - # Redirect plaintextish host to https - redirect_block = [[ - ['\n ', 'if', ' ', '($scheme != "https") '], - [['\n ', 'return', ' ', '301 https://$host$request_uri'], - '\n '] - ], ['\n']] - - self.parser.add_server_directives( - vhost, redirect_block, replace=False) - logger.info("Redirecting all traffic on port %s to ssl in %s", - self.DEFAULT_LISTEN_PORT, vhost.filep) + if self._has_certbot_redirect(vhost): + logger.info("Traffic on port %s already redirecting to ssl in %s", + self.DEFAULT_LISTEN_PORT, vhost.filep) + elif vhost.has_redirect(): + if not self._has_certbot_redirect_comment(vhost): + self._add_redirect_block(vhost, active=False) + logger.info("The appropriate server block is already redirecting " + "traffic. To enable redirect anyway, uncomment the " + "redirect lines in %s.", vhost.filep) + else: + # Redirect plaintextish host to https + self._add_redirect_block(vhost, active=True) + logger.info("Redirecting all traffic on port %s to ssl in %s", + self.DEFAULT_LISTEN_PORT, vhost.filep) def _enable_ocsp_stapling(self, domain, chain_path): """Include OCSP response in TLS handshake diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index c58a82450..4a3ca865e 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -3,6 +3,7 @@ import re from certbot.plugins import common +REDIRECT_DIRECTIVES = ['return', 'rewrite'] class Addr(common.Addr): r"""Represents an Nginx address, i.e. what comes after the 'listen' @@ -149,3 +150,32 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.path == other.path) return False + + def has_redirect(self): + """Determine if this vhost has a redirecting statement + """ + for directive_name in REDIRECT_DIRECTIVES: + found = _find_directive(self.raw, directive_name) + if found is not None: + return True + return False + + def contains_list(self, test): + """Determine if raw server block contains test list at top level + """ + for i in xrange(0, len(self.raw) - len(test)): + if self.raw[i:i + len(test)] == test: + return True + return False + +def _find_directive(directives, directive_name): + """Find a directive of type directive_name in directives + """ + if not directives or isinstance(directives, str) or len(directives) == 0: + return None + + if directives[0] == directive_name: + return directives + + matches = (_find_directive(line, directive_name) for line in directives) + return next((m for m in matches if m is not None), None) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 6203b5f71..d5664ac29 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -496,7 +496,6 @@ def parse_server(server): return parsed_server - def _add_directives(block, directives, replace): """Adds or replaces directives in a config block. diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index d871a5720..f7d6ade2d 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -445,6 +445,64 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[migration_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) + @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') + @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect') + def test_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list): + # Test that we add no redirect statement if there is already a + # redirect in the block that is managed by certbot + # Has a certbot redirect + mock_has_redirect.return_value = True + mock_contains_list.return_value = True + with mock.patch("certbot_nginx.configurator.logger") as mock_logger: + self.config.enhance("www.example.com", "redirect") + self.assertEqual(mock_logger.info.call_args[0][0], + "Traffic on port %s already redirecting to ssl in %s") + + @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') + @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect') + def test_non_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list): + # Test that we add a redirect as a comment if there is already a + # redirect-class statement in the block that isn't managed by certbot + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + + # Has a non-Certbot redirect, and has no existing comment + mock_contains_list.return_value = False + mock_has_redirect.return_value = True + with mock.patch("certbot_nginx.configurator.logger") as mock_logger: + self.config.enhance("www.example.com", "redirect") + self.assertEqual(mock_logger.info.call_args[0][0], + "The appropriate server block is already redirecting " + "traffic. To enable redirect anyway, uncomment the " + "redirect lines in %s.") + generated_conf = self.config.parser.parsed[example_conf] + expected = [ + ['#', ' Redirect non-https traffic to https'], + ['#', ' if ($scheme != "https") {'], + ['#', ' return 301 https://$host$request_uri;'], + ['#', ' } # managed by Certbot'] + ] + for line in expected: + self.assertTrue(util.contains_at_depth(generated_conf, line, 2)) + + @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') + @mock.patch('certbot_nginx.obj.VirtualHost.has_redirect') + @mock.patch('certbot_nginx.configurator.NginxConfigurator._has_certbot_redirect_comment') + @mock.patch('certbot_nginx.configurator.NginxConfigurator._add_redirect_block') + def test_redirect_comment_exists(self, mock_add_redirect_block, + mock_has_cb_redirect_comment, mock_has_redirect, mock_contains_list): + # Test that we add nothing if there is a non-Certbot redirect and a + # preexisting comment + # Has a non-Certbot redirect and a comment + mock_has_redirect.return_value = True + mock_contains_list.return_value = False # self._has_certbot_redirect(vhost): + mock_has_cb_redirect_comment.return_value = True + + # assert _add_redirect_block not called + with mock.patch("certbot_nginx.configurator.logger") as mock_logger: + self.config.enhance("www.example.com", "redirect") + self.assertFalse(mock_add_redirect_block.called) + self.assertTrue(mock_logger.info.called) + def test_redirect_dont_enhance(self): # Test that we don't accidentally add redirect to ssl-only block with mock.patch("certbot_nginx.configurator.logger") as mock_logger: diff --git a/certbot-nginx/certbot_nginx/tests/obj_test.py b/certbot-nginx/certbot_nginx/tests/obj_test.py index 84d0c6bca..b153db8d4 100644 --- a/certbot-nginx/certbot_nginx/tests/obj_test.py +++ b/certbot-nginx/certbot_nginx/tests/obj_test.py @@ -87,10 +87,43 @@ class VirtualHostTest(unittest.TestCase): def setUp(self): from certbot_nginx.obj import VirtualHost from certbot_nginx.obj import Addr + raw1 = [ + ['listen', '69.50.225.155:9000'], + [['if', '($scheme != "https") '], + [['return', '301 https://$host$request_uri']] + ], + ['#', ' managed by Certbot'] + ] self.vhost1 = VirtualHost( "filep", set([Addr.fromstring("localhost")]), False, False, - set(['localhost']), [], []) + set(['localhost']), raw1, []) + raw2 = [ + ['listen', '69.50.225.155:9000'], + [['if', '($scheme != "https") '], + [['return', '301 https://$host$request_uri']] + ] + ] + self.vhost2 = VirtualHost( + "filep", + set([Addr.fromstring("localhost")]), False, False, + set(['localhost']), raw2, []) + raw3 = [ + ['listen', '69.50.225.155:9000'], + ['rewrite', '^(.*)$ $scheme://www.domain.com$1 permanent;'] + ] + self.vhost3 = VirtualHost( + "filep", + set([Addr.fromstring("localhost")]), False, False, + set(['localhost']), raw3, []) + raw4 = [ + ['listen', '69.50.225.155:9000'], + ['server_name', 'return.com'] + ] + self.vhost4 = VirtualHost( + "filp", + set([Addr.fromstring("localhost")]), False, False, + set(['localhost']), raw4, []) def test_eq(self): from certbot_nginx.obj import Addr @@ -110,6 +143,47 @@ class VirtualHostTest(unittest.TestCase): 'enabled: False']) self.assertEqual(stringified, str(self.vhost1)) + def test_has_redirect(self): + self.assertTrue(self.vhost1.has_redirect()) + self.assertTrue(self.vhost2.has_redirect()) + self.assertTrue(self.vhost3.has_redirect()) + self.assertFalse(self.vhost4.has_redirect()) + + def test_contains_list(self): + from certbot_nginx.obj import VirtualHost + from certbot_nginx.obj import Addr + from certbot_nginx.configurator import TEST_REDIRECT_BLOCK + test_needle = TEST_REDIRECT_BLOCK + test_haystack = [['listen', '80'], ['root', '/var/www/html'], + ['index', 'index.html index.htm index.nginx-debian.html'], + ['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'], + ['#', ' managed by Certbot'], + ['ssl_certificate', '/etc/letsencrypt/live/two.functorkitten.xyz/fullchain.pem'], + ['#', ' managed by Certbot'], + ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'], + ['#', ' managed by Certbot'], + [['if', '($scheme != "https")'], [['return', '301 https://$host$request_uri']]], + ['#', ' managed by Certbot'], []] + vhost_haystack = VirtualHost( + "filp", + set([Addr.fromstring("localhost")]), False, False, + set(['localhost']), test_haystack, []) + test_bad_haystack = [['listen', '80'], ['root', '/var/www/html'], + ['index', 'index.html index.htm index.nginx-debian.html'], + ['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'], + ['#', ' managed by Certbot'], + ['ssl_certificate', '/etc/letsencrypt/live/two.functorkitten.xyz/fullchain.pem'], + ['#', ' managed by Certbot'], + ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'], + ['#', ' managed by Certbot'], + [['if', '($scheme != "https")'], [['return', '302 https://$host$request_uri']]], + ['#', ' managed by Certbot'], []] + vhost_bad_haystack = VirtualHost( + "filp", + set([Addr.fromstring("localhost")]), False, False, + set(['localhost']), test_bad_haystack, []) + self.assertTrue(vhost_haystack.contains_list(test_needle)) + self.assertFalse(vhost_bad_haystack.contains_list(test_needle)) if __name__ == "__main__": unittest.main() # pragma: no cover From 2b229d4b9dacadcf8f4f084e1dd4c7c7dae3f73c Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Mon, 7 Nov 2016 16:14:09 -0800 Subject: [PATCH 252/331] Allow notification interface to not wrap text (#3728) --- certbot/display/util.py | 12 ++++++++---- certbot/interfaces.py | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 0ad8fa200..47bce87b4 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -55,17 +55,19 @@ class FileDisplay(object): super(FileDisplay, self).__init__() self.outfile = outfile - def notification(self, message, pause=True): + def notification(self, message, pause=True, wrap=True): # pylint: disable=unused-argument """Displays a notification and waits for user acceptance. :param str message: Message to display :param bool pause: Whether or not the program should pause for the user's confirmation + :param bool wrap: Whether or not the application should wrap text """ side_frame = "-" * 79 - message = _wrap_lines(message) + if wrap: + message = _wrap_lines(message) self.outfile.write( "{line}{frame}{line}{msg}{line}{frame}{line}".format( line=os.linesep, frame=side_frame, msg=message)) @@ -322,16 +324,18 @@ class NoninteractiveDisplay(object): msg += "\n\n(You can set this with the {0} flag)".format(cli_flag) raise errors.MissingCommandlineFlag(msg) - def notification(self, message, pause=False): + def notification(self, message, pause=False, wrap=True): # pylint: disable=unused-argument """Displays a notification without waiting for user acceptance. :param str message: Message to display to stdout :param bool pause: The NoninteractiveDisplay waits for no keyboard + :param bool wrap: Whether or not the application should wrap text """ side_frame = "-" * 79 - message = _wrap_lines(message) + if wrap: + message = _wrap_lines(message) self.outfile.write( "{line}{frame}{line}{msg}{line}{frame}{line}".format( line=os.linesep, frame=side_frame, msg=message)) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 337bb3238..04843c694 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -365,12 +365,13 @@ class IInstaller(IPlugin): class IDisplay(zope.interface.Interface): """Generic display.""" - def notification(message, pause): + def notification(message, pause, wrap=True): """Displays a string message :param str message: Message to display :param bool pause: Whether or not the application should pause for confirmation (if available) + :param bool wrap: Whether or not the application should wrap text """ From a7bfefc6d009b3244e6f41a2dca2035c580fe4d8 Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Tue, 8 Nov 2016 14:09:20 -0500 Subject: [PATCH 253/331] Change all "cerbot" references to "certbot" (#3770) --- docs/using.rst | 2 +- tools/release.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 49f30ff58..7c1fac003 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -129,7 +129,7 @@ the webserver. Nginx ----- -The Nginx plugin has been distributed with Cerbot since version 0.9.0 and should +The Nginx plugin has been distributed with Certbot since version 0.9.0 and should work for most configurations. Because it is alpha code, we recommend backing up Nginx configurations before using it (though you can also revert changes to configurations with ``certbot --nginx rollback``). You can use it by providing diff --git a/tools/release.sh b/tools/release.sh index 57985d7a4..c4bb61f2f 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -195,7 +195,7 @@ done # This signature is not quite as strong, but easier for people to verify out of band gpg -u "$RELEASE_GPG_KEY" --detach-sign --armor --sign letsencrypt-auto-source/letsencrypt-auto # We can't rename the openssl letsencrypt-auto.sig for compatibility reasons, -# but we can use the right name for cerbot-auto.asc from day one +# but we can use the right name for certbot-auto.asc from day one mv letsencrypt-auto-source/letsencrypt-auto.asc letsencrypt-auto-source/certbot-auto.asc # copy leauto to the root, overwriting the previous release version From af46f644a71b9778e4295a84c3e22e75bbb55f6c Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Tue, 8 Nov 2016 15:21:42 -0800 Subject: [PATCH 254/331] Add list-certs command (#3669) * Switch to using absolute path in symlink * save archive_dir to config and read it back * cli_config.archive_dir --> cli_config.default_archive_dir * Use archive_dir specified in renewal config file * add helpful broken symlink info * add docstring to method * Add tests * remove extraneous test imports * fix tests * py2.6 syntax fix * git problems * Add list-certs command * no dict comprehension in python2.6 * add test coverage * More py26 wrangling * update tests for py3 and lint * remove extra dep from test * test coverage * test shouldn't be based on dict representation order * Redo report UX and add tests to cover * remove storage str test * lint and use mock properly * mock properly * address code review comments * lineage --> certificate name and print fullchain and privkey paths * make py26 happy * actually make py26 happy * don't wrap text --- certbot/cert_manager.py | 84 ++++++++++++++++++++++++++ certbot/cli.py | 4 +- certbot/main.py | 5 ++ certbot/renewal.py | 2 +- certbot/storage.py | 12 +++- certbot/tests/cert_manager_test.py | 97 +++++++++++++++++++++++++++--- certbot/tests/cli_test.py | 5 ++ certbot/tests/storage_test.py | 1 - 8 files changed, 197 insertions(+), 13 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 94714bc98..a3237253e 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -1,8 +1,17 @@ """Tools for managing certificates.""" +import datetime +import logging +import pytz +import traceback +import zope.component + from certbot import configuration +from certbot import interfaces from certbot import renewal from certbot import storage +logger = logging.getLogger(__name__) + def update_live_symlinks(config): """Update the certificate file family symlinks to use archive_dir. @@ -20,3 +29,78 @@ def update_live_symlinks(config): storage.RenewableCert(renewal_file, configuration.RenewerConfiguration(renewer_config), update_symlinks=True) + +def _report_lines(msgs): + """Format a results report for a category of single-line renewal outcomes""" + return " " + "\n ".join(str(msg) for msg in msgs) + +def _report_human_readable(parsed_certs): + """Format a results report for a parsed cert""" + certinfo = [] + for cert in parsed_certs: + now = pytz.UTC.fromutc(datetime.datetime.utcnow()) + if cert.target_expiry <= now: + expiration_text = "EXPIRED" + else: + diff = cert.target_expiry - now + if diff.days == 1: + expiration_text = "1 day" + elif diff.days < 1: + expiration_text = "under 1 day" + else: + expiration_text = "{0} days".format(diff.days) + valid_string = "{0} ({1})".format(cert.target_expiry, expiration_text) + certinfo.append(" Certificate Name: {0}\n" + " Domains: {1}\n" + " Valid Until: {2}\n" + " Certificate Path: {3}\n" + " Private Key Path: {4}".format( + cert.lineagename, + " ".join(cert.names()), + valid_string, + cert.fullchain, + cert.privkey)) + return "\n".join(certinfo) + +def _describe_certs(parsed_certs, parse_failures): + """Print information about the certs we know about""" + out = [] + + notify = out.append + + if not parsed_certs and not parse_failures: + notify("No certs found.") + else: + if parsed_certs: + notify("Found the following certs:") + notify(_report_human_readable(parsed_certs)) + if parse_failures: + notify("\nThe following renewal configuration files " + "were invalid:") + notify(_report_lines(parse_failures)) + + disp = zope.component.getUtility(interfaces.IDisplay) + disp.notification("\n".join(out), pause=False, wrap=False) + +def certificates(config): + """Display information about certs configured with Certbot + + :param config: Configuration. + :type config: :class:`certbot.interfaces.IConfig` + """ + renewer_config = configuration.RenewerConfiguration(config) + parsed_certs = [] + parse_failures = [] + for renewal_file in renewal.renewal_conf_files(renewer_config): + try: + renewal_candidate = storage.RenewableCert(renewal_file, + configuration.RenewerConfiguration(config)) + parsed_certs.append(renewal_candidate) + except Exception as e: # pylint: disable=broad-except + logger.warning("Renewal configuration file %s produced an " + "unexpected error: %s. Skipping.", renewal_file, e) + logger.debug("Traceback was:\n%s", traceback.format_exc()) + parse_failures.append(renewal_file) + + # Describe all the certs + _describe_certs(parsed_certs, parse_failures) diff --git a/certbot/cli.py b/certbot/cli.py index 41afa1391..fa3bcc48b 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -69,6 +69,7 @@ cert. Major SUBCOMMANDS are: config_changes Show changes made to server config during installation update_symlinks Update cert symlinks based on renewal config file plugins Display information about installed plugins + certificates Display information about certs configured with Certbot """.format(cli_command) @@ -324,7 +325,8 @@ class HelpfulArgumentParser(object): "install": main.install, "plugins": main.plugins_cmd, "register": main.register, "renew": main.renew, "revoke": main.revoke, "rollback": main.rollback, - "everything": main.run, "update_symlinks": main.update_symlinks} + "everything": main.run, "update_symlinks": main.update_symlinks, + "certificates": main.certificates} # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) diff --git a/certbot/main.py b/certbot/main.py index b651249fa..39c19bd7a 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -484,6 +484,11 @@ def update_symlinks(config, unused_plugins): """ cert_manager.update_live_symlinks(config) +def certificates(config, unused_plugins): + """Display information about certs configured with Certbot + """ + cert_manager.certificates(config) + def revoke(config, unused_plugins): # TODO: coop with renewal config """Revoke a previously obtained certificate.""" # For user-agent construction diff --git a/certbot/renewal.py b/certbot/renewal.py index f5b2efa46..aa39c5fad 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -44,7 +44,7 @@ def _reconstitute(config, full_path): """Try to instantiate a RenewableCert, updating config with relevant items. This is specifically for use in renewal and enforces several checks - and policies to ensure that we can try to proceed with the renwal + and policies to ensure that we can try to proceed with the renewal request. The config argument is modified by including relevant options read from the renewal configuration file. diff --git a/certbot/storage.py b/certbot/storage.py index 5d7eeb88a..7b2f575b7 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -263,6 +263,15 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes self._update_symlinks() self._check_symlinks() + @property + def target_expiry(self): + """The current target certificate's expiration datetime + + :returns: Expiration datetime of the current target certificate + :rtype: :class:`datetime.datetime` + """ + return crypto_util.notAfter(self.current_target("cert")) + @property def archive_dir(self): """Returns the default or specified archive directory""" @@ -671,9 +680,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if self.has_pending_deployment(): interval = self.configuration.get("deploy_before_expiry", "5 days") - expiry = crypto_util.notAfter(self.current_target("cert")) now = pytz.UTC.fromutc(datetime.datetime.utcnow()) - if expiry < add_time_interval(now, interval): + if self.target_expiry < add_time_interval(now, interval): return True return False diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 46f555aac..c67ab5e50 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -8,25 +8,21 @@ import unittest import configobj import mock -from certbot import configuration from certbot.storage import ALL_FOUR -class CertManagerTest(unittest.TestCase): - """Tests for certbot.cert_manager +class BaseCertManagerTest(unittest.TestCase): + """Base class for setting up Cert Manager tests. """ def setUp(self): self.tempdir = tempfile.mkdtemp() os.makedirs(os.path.join(self.tempdir, "renewal")) - mock_namespace = mock.MagicMock( + self.cli_config = mock.MagicMock( config_dir=self.tempdir, work_dir=self.tempdir, logs_dir=self.tempdir, - ) - - self.cli_config = configuration.RenewerConfiguration( - namespace=mock_namespace + quiet=False, ) self.domains = { @@ -67,6 +63,9 @@ class CertManagerTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tempdir) +class UpdateLiveSymlinksTest(BaseCertManagerTest): + """Tests for certbot.cert_manager.update_live_symlinks + """ def test_update_live_symlinks(self): """Test update_live_symlinks""" # pylint: disable=too-many-statements @@ -97,5 +96,87 @@ class CertManagerTest(unittest.TestCase): self.assertEqual(os.readlink(self.configs[domain][kind]), archive_paths[domain][kind]) +class CertificatesTest(BaseCertManagerTest): + """Tests for certbot.cert_manager.certificates + """ + def _certificates(self, *args, **kwargs): + from certbot.cert_manager import certificates + return certificates(*args, **kwargs) + + @mock.patch('certbot.cert_manager.logger') + @mock.patch('zope.component.getUtility') + def test_certificates_parse_fail(self, mock_utility, mock_logger): + self._certificates(self.cli_config) + self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member + self.assertTrue(mock_utility.called) + + @mock.patch('certbot.cert_manager.logger') + @mock.patch('zope.component.getUtility') + def test_certificates_quiet(self, mock_utility, mock_logger): + self.cli_config.quiet = True + self._certificates(self.cli_config) + self.assertFalse(mock_utility.notification.called) + self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member + + @mock.patch('certbot.cert_manager.logger') + @mock.patch('zope.component.getUtility') + @mock.patch("certbot.storage.RenewableCert") + @mock.patch('certbot.cert_manager._report_human_readable') + def test_certificates_parse_success(self, mock_report, mock_renewable_cert, + mock_utility, mock_logger): + mock_report.return_value = "" + self._certificates(self.cli_config) + self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member + self.assertTrue(mock_report.called) + self.assertTrue(mock_utility.called) + self.assertTrue(mock_renewable_cert.called) + + @mock.patch('certbot.cert_manager.logger') + @mock.patch('zope.component.getUtility') + def test_certificates_no_files(self, mock_utility, mock_logger): + tempdir = tempfile.mkdtemp() + + cli_config = mock.MagicMock( + config_dir=tempdir, + work_dir=tempdir, + logs_dir=tempdir, + quiet=False, + ) + + os.makedirs(os.path.join(tempdir, "renewal")) + self._certificates(cli_config) + self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member + self.assertTrue(mock_utility.called) + shutil.rmtree(tempdir) + + def test_report_human_readable(self): + from certbot import cert_manager + import datetime, pytz + expiry = pytz.UTC.fromutc(datetime.datetime.utcnow()) + + cert = mock.MagicMock(lineagename="nameone") + cert.target_expiry = expiry + cert.names.return_value = ["nameone", "nametwo"] + parsed_certs = [cert] + # pylint: disable=protected-access + out = cert_manager._report_human_readable(parsed_certs) + self.assertTrue('EXPIRED' in out) + + cert.target_expiry += datetime.timedelta(hours=2) + # pylint: disable=protected-access + out = cert_manager._report_human_readable(parsed_certs) + self.assertTrue('under 1 day' in out) + + cert.target_expiry += datetime.timedelta(days=1) + # pylint: disable=protected-access + out = cert_manager._report_human_readable(parsed_certs) + self.assertTrue('1 day' in out) + self.assertFalse('under' in out) + + cert.target_expiry += datetime.timedelta(days=2) + # pylint: disable=protected-access + out = cert_manager._report_human_readable(parsed_certs) + self.assertTrue('3 days' in out) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 63abe6451..8d4d0af62 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -277,6 +277,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call_no_clientmock(['update_symlinks']) self.assertEqual(1, mock_cert_manager.call_count) + @mock.patch('certbot.cert_manager.certificates') + def test_certificates(self, mock_cert_manager): + self._call_no_clientmock(['certificates']) + self.assertEqual(1, mock_cert_manager.call_count) + def test_plugins(self): flags = ['--init', '--prepare', '--authenticators', '--installers'] for args in itertools.chain( diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index fb33a1864..4d7323e66 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -751,6 +751,5 @@ class RenewableCertTests(BaseRenewableCertTest): storage.RenewableCert(self.config.filename, self.cli_config, update_symlinks=True) - if __name__ == "__main__": unittest.main() # pragma: no cover From 8c1aa3ef466f2c76b71f236d9c7f777e70e6bfc1 Mon Sep 17 00:00:00 2001 From: mstrache Date: Wed, 9 Nov 2016 01:48:12 +0100 Subject: [PATCH 255/331] #3408: Made Gentoo bootstrapping asking before performing any changes (#3410) * #3408: Made gentoo bootstrapping ask before it performs any changes * Update gentoo_common.sh Removed use of the local keyword --- .../pieces/bootstrappers/gentoo_common.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh index 4b1e3b545..86a1ec7d6 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh @@ -8,15 +8,20 @@ BootstrapGentooCommon() { app-misc/ca-certificates virtual/pkgconfig" + ASK_OPTION="--ask" + if [ "$ASSUME_YES" = 1 ]; then + ASK_OPTION="" + fi + case "$PACKAGE_MANAGER" in (paludis) $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x ;; (pkgcore) - $SUDO pmerge --noreplace --oneshot $PACKAGES + $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES ;; (portage|*) - $SUDO emerge --noreplace --oneshot $PACKAGES + $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES ;; esac } From dd8772b608d0f21569145e8b502df72d5d73fff1 Mon Sep 17 00:00:00 2001 From: Nick Fong Date: Tue, 8 Nov 2016 17:19:05 -0800 Subject: [PATCH 256/331] Remove get_all_certs_keys() from Apache and Nginx (#3768) - Remove get_all_certs_keys() implementation in - certbot-apache/certbot_apache/configurator.py - Remove corresponding tests for get_all_certs_keys() in - certbot-apache/certbot_apache/tests/configurator_test.py - Remove get_all_certs_keys() implementation in - certbot-nginx/certbot_nginx/configurator.py - certbot-nginx/certbot_nginx/parser.py - Remove corresponding tests for get_all_certs_keys() in: - certbot-nginx/certbot_nginx/tests/configurator_test.py - certbot-nginx/certbot_nginx/tests/parser_test.py Resolves #3762 --- certbot-apache/certbot_apache/configurator.py | 32 ----------------- .../certbot_apache/tests/configurator_test.py | 15 -------- certbot-nginx/certbot_nginx/configurator.py | 12 ------- certbot-nginx/certbot_nginx/parser.py | 27 -------------- .../certbot_nginx/tests/configurator_test.py | 35 ------------------- .../certbot_nginx/tests/parser_test.py | 20 ----------- 6 files changed, 141 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 75fbe3456..1bb0a1e1a 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -1494,38 +1494,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return redirects - def get_all_certs_keys(self): - """Find all existing keys, certs from configuration. - - Retrieve all certs and keys set in VirtualHosts on the Apache server - - :returns: list of tuples with form [(cert, key, path)] - cert - str path to certificate file - key - str path to associated key file - path - File path to configuration file. - :rtype: list - - """ - c_k = set() - - for vhost in self.vhosts: - if vhost.ssl: - cert_path = self.parser.find_dir( - "SSLCertificateFile", None, - start=vhost.path, exclude=False) - key_path = self.parser.find_dir( - "SSLCertificateKeyFile", None, - start=vhost.path, exclude=False) - - if cert_path and key_path: - cert = os.path.abspath(self.parser.get_arg(cert_path[-1])) - key = os.path.abspath(self.parser.get_arg(key_path[-1])) - c_k.add((cert, key, get_file_path(cert_path[-1]))) - else: - logger.warning( - "Invalid VirtualHost configuration - %s", vhost.filep) - return c_k - def is_site_enabled(self, avail_fp): """Checks to see if the given site is enabled. diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index dc953174e..5f4685e96 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -775,21 +775,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises(errors.MisconfigurationError, self.config.config_test) - def test_get_all_certs_keys(self): - c_k = self.config.get_all_certs_keys() - self.assertEqual(len(c_k), 3) - cert, key, path = next(iter(c_k)) - self.assertTrue("cert" in cert) - self.assertTrue("key" in key) - self.assertTrue("default-ssl" in path or "ocsp-ssl" in path) - - def test_get_all_certs_keys_malformed_conf(self): - self.config.parser.find_dir = mock.Mock( - side_effect=[["path"], [], ["path"], [], ["path"], []]) - c_k = self.config.get_all_certs_keys() - - self.assertFalse(c_k) - def test_more_info(self): self.assertTrue(self.config.more_info()) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 94ac7de86..0c6e1598c 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -474,18 +474,6 @@ class NginxConfigurator(common.Plugin): self.parser.add_server_directives( vhost, ssl_block, replace=False) - def get_all_certs_keys(self): - """Find all existing keys, certs from configuration. - - :returns: list of tuples with form [(cert, key, path)] - cert - str path to certificate file - key - str path to associated key file - path - File path to configuration file. - :rtype: set - - """ - return self.parser.get_all_certs_keys() - ################################## # enhancement methods (IInstaller) ################################## diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index d5664ac29..385635212 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -298,33 +298,6 @@ class NginxParser(object): except errors.MisconfigurationError as err: raise errors.MisconfigurationError("Problem in %s: %s" % (filename, err.message)) - def get_all_certs_keys(self): - """Gets all certs and keys in the nginx config. - - :returns: list of tuples with form [(cert, key, path)] - cert - str path to certificate file - key - str path to associated key file - path - File path to configuration file. - :rtype: set - - """ - c_k = set() - vhosts = self.get_vhosts() - for vhost in vhosts: - tup = [None, None, vhost.filep] - if vhost.ssl: - for directive in vhost.raw: - # A directive can be an empty list to preserve whitespace - if not directive: - continue - if directive[0] == 'ssl_certificate': - tup[0] = directive[1] - elif directive[0] == 'ssl_certificate_key': - tup[1] = directive[1] - if tup[0] is not None and tup[1] is not None: - c_k.add(tuple(tup)) - return c_k - def _do_for_subarray(entry, condition, func, path=None): """Executes a function for a subarray of a nested array if it matches diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index f7d6ade2d..f165ea23a 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -238,41 +238,6 @@ class NginxConfiguratorTest(util.NginxTest): ], parsed_migration_conf[0]) - def test_get_all_certs_keys(self): - nginx_conf = self.config.parser.abs_path('nginx.conf') - example_conf = self.config.parser.abs_path('sites-enabled/example.com') - migration_conf = self.config.parser.abs_path('sites-enabled/migration.com') - sslon_conf = self.config.parser.abs_path('sites-enabled/sslon.com') - - # Get the default SSL vhost - self.config.deploy_cert( - "www.example.com", - "example/cert.pem", - "example/key.pem", - "example/chain.pem", - "example/fullchain.pem") - self.config.deploy_cert( - "another.alias", - "/etc/nginx/cert.pem", - "/etc/nginx/key.pem", - "/etc/nginx/chain.pem", - "/etc/nginx/fullchain.pem") - self.config.deploy_cert( - "migration.com", - "migration/cert.pem", - "migration/key.pem", - "migration/chain.pem", - "migration/fullchain.pem") - self.config.save() - - self.config.parser.load() - self.assertEqual(set([ - ('example/fullchain.pem', 'example/key.pem', example_conf), - ('/etc/nginx/fullchain.pem', '/etc/nginx/key.pem', nginx_conf), - ('migration/fullchain.pem', 'migration/key.pem', migration_conf), - ('snakeoil.cert', 'snakeoil.key', sslon_conf), - ]), self.config.get_all_certs_keys()) - @mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform") @mock.patch("certbot_nginx.configurator.NginxConfigurator.restart") @mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config") diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index d5593171a..54deffd7a 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -291,26 +291,6 @@ class NginxParserTest(util.NginxTest): COMMENT_BLOCK, ["\n", "e", " ", "f"]]) - def test_get_all_certs_keys(self): - nparser = parser.NginxParser(self.config_path, self.ssl_options) - filep = nparser.abs_path('sites-enabled/example.com') - mock_vhost = obj.VirtualHost(filep, - None, None, None, - set(['.example.com', 'example.*']), - None, [0]) - nparser.add_server_directives(mock_vhost, - [['ssl_certificate', 'foo.pem'], - ['ssl_certificate_key', 'bar.key'], - ['listen', '443 ssl']], - replace=False) - c_k = nparser.get_all_certs_keys() - migration_file = nparser.abs_path('sites-enabled/migration.com') - sslon_file = nparser.abs_path('sites-enabled/sslon.com') - self.assertEqual(set([('foo.pem', 'bar.key', filep), - ('cert.pem', 'cert.key', migration_file), - ('snakeoil.cert', 'snakeoil.key', sslon_file) - ]), c_k) - def test_parse_server_ssl(self): server = parser.parse_server([ ['listen', '443'] From 469b5fd441cae085e922c8e66815b5094747a7c5 Mon Sep 17 00:00:00 2001 From: Nick Fong Date: Tue, 8 Nov 2016 17:21:49 -0800 Subject: [PATCH 257/331] Remove letsencrypt[-apache|-nginx] (#3769) --- letsencrypt-apache/LICENSE.txt | 190 --------------- letsencrypt-apache/MANIFEST.in | 2 - letsencrypt-apache/README.rst | 2 - .../letsencrypt_apache/__init__.py | 8 - letsencrypt-apache/setup.py | 59 ----- letsencrypt-nginx/LICENSE.txt | 216 ------------------ letsencrypt-nginx/MANIFEST.in | 2 - letsencrypt-nginx/README.rst | 2 - .../letsencrypt_nginx/__init__.py | 8 - letsencrypt-nginx/setup.py | 59 ----- letsencrypt/LICENSE.txt | 205 ----------------- letsencrypt/MANIFEST.in | 2 - letsencrypt/README.rst | 2 - letsencrypt/letsencrypt/__init__.py | 8 - letsencrypt/setup.py | 62 ----- letsencrypt/tests/testdata/os-release | 8 - 16 files changed, 835 deletions(-) delete mode 100644 letsencrypt-apache/LICENSE.txt delete mode 100644 letsencrypt-apache/MANIFEST.in delete mode 100644 letsencrypt-apache/README.rst delete mode 100644 letsencrypt-apache/letsencrypt_apache/__init__.py delete mode 100644 letsencrypt-apache/setup.py delete mode 100644 letsencrypt-nginx/LICENSE.txt delete mode 100644 letsencrypt-nginx/MANIFEST.in delete mode 100644 letsencrypt-nginx/README.rst delete mode 100644 letsencrypt-nginx/letsencrypt_nginx/__init__.py delete mode 100644 letsencrypt-nginx/setup.py delete mode 100644 letsencrypt/LICENSE.txt delete mode 100644 letsencrypt/MANIFEST.in delete mode 100644 letsencrypt/README.rst delete mode 100644 letsencrypt/letsencrypt/__init__.py delete mode 100644 letsencrypt/setup.py delete mode 100644 letsencrypt/tests/testdata/os-release diff --git a/letsencrypt-apache/LICENSE.txt b/letsencrypt-apache/LICENSE.txt deleted file mode 100644 index 981c46c9f..000000000 --- a/letsencrypt-apache/LICENSE.txt +++ /dev/null @@ -1,190 +0,0 @@ - Copyright 2015 Electronic Frontier Foundation and others - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/letsencrypt-apache/MANIFEST.in b/letsencrypt-apache/MANIFEST.in deleted file mode 100644 index 97e2ad3df..000000000 --- a/letsencrypt-apache/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include LICENSE.txt -include README.rst diff --git a/letsencrypt-apache/README.rst b/letsencrypt-apache/README.rst deleted file mode 100644 index c0c201f14..000000000 --- a/letsencrypt-apache/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -This package is a simple shim for backwards compatibility around -``certbot-apache``, the Apache plugin for ``certbot``. diff --git a/letsencrypt-apache/letsencrypt_apache/__init__.py b/letsencrypt-apache/letsencrypt_apache/__init__.py deleted file mode 100644 index cc8faef21..000000000 --- a/letsencrypt-apache/letsencrypt_apache/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Let's Encrypt Apache plugin.""" -import sys - - -import certbot_apache - - -sys.modules['letsencrypt_apache'] = certbot_apache diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py deleted file mode 100644 index 09703841c..000000000 --- a/letsencrypt-apache/setup.py +++ /dev/null @@ -1,59 +0,0 @@ -import codecs -import os -import sys - -from setuptools import setup -from setuptools import find_packages - - -def read_file(filename, encoding='utf8'): - """Read unicode from given file.""" - with codecs.open(filename, encoding=encoding) as fd: - return fd.read() - - -here = os.path.abspath(os.path.dirname(__file__)) -readme = read_file(os.path.join(here, 'README.rst')) - - -version = '0.8.0.dev0' - - -# This package is a simple shim around certbot-apache -install_requires = [ - 'certbot-apache', - 'letsencrypt=={0}'.format(version), -] - - -setup( - name='letsencrypt-apache', - version=version, - description="Apache plugin for Let's Encrypt", - long_description=readme, - url='https://github.com/letsencrypt/letsencrypt', - author="Certbot Project", - author_email='client-dev@letsencrypt.org', - license='Apache License 2.0', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Environment :: Plugins', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Security', - 'Topic :: System :: Installation/Setup', - 'Topic :: System :: Networking', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities', - ], - - packages=find_packages(), - include_package_data=True, - install_requires=install_requires, -) diff --git a/letsencrypt-nginx/LICENSE.txt b/letsencrypt-nginx/LICENSE.txt deleted file mode 100644 index 02a1459be..000000000 --- a/letsencrypt-nginx/LICENSE.txt +++ /dev/null @@ -1,216 +0,0 @@ - Copyright 2015 Electronic Frontier Foundation and others - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Incorporating code from nginxparser - Copyright 2014 Fatih Erikli - Licensed MIT - - -Text of Apache License -====================== - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - -Text of MIT License -=================== -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/letsencrypt-nginx/MANIFEST.in b/letsencrypt-nginx/MANIFEST.in deleted file mode 100644 index 97e2ad3df..000000000 --- a/letsencrypt-nginx/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include LICENSE.txt -include README.rst diff --git a/letsencrypt-nginx/README.rst b/letsencrypt-nginx/README.rst deleted file mode 100644 index cd1f32fb8..000000000 --- a/letsencrypt-nginx/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -This package is a simple shim for backwards compatibility around -``certbot-nginx``, the Nginx plugin for ``certbot``. diff --git a/letsencrypt-nginx/letsencrypt_nginx/__init__.py b/letsencrypt-nginx/letsencrypt_nginx/__init__.py deleted file mode 100644 index aa14fe963..000000000 --- a/letsencrypt-nginx/letsencrypt_nginx/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Let's Encrypt Nginx plugin.""" -import sys - - -import certbot_nginx - - -sys.modules['letsencrypt_nginx'] = certbot_nginx diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py deleted file mode 100644 index 25db12a47..000000000 --- a/letsencrypt-nginx/setup.py +++ /dev/null @@ -1,59 +0,0 @@ -import codecs -import os -import sys - -from setuptools import setup -from setuptools import find_packages - - -def read_file(filename, encoding='utf8'): - """Read unicode from given file.""" - with codecs.open(filename, encoding=encoding) as fd: - return fd.read() - - -here = os.path.abspath(os.path.dirname(__file__)) -readme = read_file(os.path.join(here, 'README.rst')) - - -version = '0.8.0.dev0' - - -# This package is a simple shim around certbot-nginx -install_requires = [ - 'certbot-nginx', - 'letsencrypt=={0}'.format(version), -] - - -setup( - name='letsencrypt-nginx', - version=version, - description="Nginx plugin for Let's Encrypt", - long_description=readme, - url='https://github.com/letsencrypt/letsencrypt', - author="Certbot Project", - author_email='client-dev@letsencrypt.org', - license='Apache License 2.0', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Environment :: Plugins', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Security', - 'Topic :: System :: Installation/Setup', - 'Topic :: System :: Networking', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities', - ], - - packages=find_packages(), - include_package_data=True, - install_requires=install_requires, -) diff --git a/letsencrypt/LICENSE.txt b/letsencrypt/LICENSE.txt deleted file mode 100644 index 82d868261..000000000 --- a/letsencrypt/LICENSE.txt +++ /dev/null @@ -1,205 +0,0 @@ -Let's Encrypt ACME Client -Copyright (c) Electronic Frontier Foundation and others -Licensed Apache Version 2.0 - -The nginx plugin incorporates code from nginxparser -Copyright (c) 2014 Fatih Erikli -Licensed MIT - - -Text of Apache License -====================== - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - -Text of MIT License -=================== -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/letsencrypt/MANIFEST.in b/letsencrypt/MANIFEST.in deleted file mode 100644 index 97e2ad3df..000000000 --- a/letsencrypt/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include LICENSE.txt -include README.rst diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst deleted file mode 100644 index b5fa0ec95..000000000 --- a/letsencrypt/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -This package is a simple shim around the ``certbot`` ACME client for backwards -compatibility. diff --git a/letsencrypt/letsencrypt/__init__.py b/letsencrypt/letsencrypt/__init__.py deleted file mode 100644 index a67d641f5..000000000 --- a/letsencrypt/letsencrypt/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Let's Encrypt ACME client.""" -import sys - - -import certbot - - -sys.modules['letsencrypt'] = certbot diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py deleted file mode 100644 index 4541e85fe..000000000 --- a/letsencrypt/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -import codecs -import os -import sys - -from setuptools import setup -from setuptools import find_packages - - -def read_file(filename, encoding='utf8'): - """Read unicode from given file.""" - with codecs.open(filename, encoding=encoding) as fd: - return fd.read() - - -here = os.path.abspath(os.path.dirname(__file__)) -readme = read_file(os.path.join(here, 'README.rst')) - - -# This package is a simple shim around certbot -install_requires = ['certbot'] - - -version = '0.8.0.dev0' - - -setup( - name='letsencrypt', - version=version, - description="ACME client", - long_description=readme, - url='https://github.com/letsencrypt/letsencrypt', - author="Certbot Project", - author_email='client-dev@letsencrypt.org', - license='Apache License 2.0', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Environment :: Console', - 'Environment :: Console :: Curses', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Security', - 'Topic :: System :: Installation/Setup', - 'Topic :: System :: Networking', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities', - ], - - packages=find_packages(), - include_package_data=True, - install_requires=install_requires, - entry_points={ - 'console_scripts': [ - 'letsencrypt = certbot.main:main', - ], - }, -) diff --git a/letsencrypt/tests/testdata/os-release b/letsencrypt/tests/testdata/os-release deleted file mode 100644 index b7c3ceb1b..000000000 --- a/letsencrypt/tests/testdata/os-release +++ /dev/null @@ -1,8 +0,0 @@ -NAME="SystemdOS" -VERSION="42.42.42 LTS, Unreal" -ID=systemdos -ID_LIKE=debian -PRETTY_NAME="SystemdOS 42.42.42 Unreal" -VERSION_ID="42" -HOME_URL="http://www.example.com/" -SUPPORT_URL="http://help.example.com/" From 04bec308fb126860a28689579cc449d7646bb42a Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Wed, 9 Nov 2016 12:55:18 -0800 Subject: [PATCH 258/331] Add README file to each live directory explaining its contents. (#3696) * Add README file to each live directory explaining its contents. * add tests * Update README copy * add fragment * update copy * lint errors --- certbot/storage.py | 15 +++++++++++++++ certbot/tests/storage_test.py | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/certbot/storage.py b/certbot/storage.py index 7b2f575b7..1fc13a5df 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -841,6 +841,21 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(cert + chain) + # Write a README file to the live directory + readme_path = os.path.join(live_dir, "README") + with open(readme_path, "w") as f: + logger.debug("Writing README to %s.", readme_path) + f.write("This directory contains your keys and certificates.\n\n" + "`privkey.pem` : the private key for your certificate.\n" + "`fullchain.pem`: the certificate file used in most server software.\n" + "`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.\n" + "`cert.pem` : will break many server configurations, and " + "should not be used\n" + " without reading further documentation (see link below).\n\n" + "We recommend not moving these files. For more information, see the Certbot\n" + "User Guide at https://certbot.eff.org/docs/using.html#where-are-my-" + "certificates.\n") + # Document what we've done in a new renewal config file config_file.close() diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 4d7323e66..46e2aff0d 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -580,6 +580,8 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertTrue(result._consistent()) self.assertTrue(os.path.exists(os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.live_dir, "the-lineage.com", "README"))) with open(result.fullchain, "rb") as f: self.assertEqual(f.read(), b"cert" + b"chain") # Let's do it again and make sure it makes a different lineage @@ -587,6 +589,8 @@ class RenewableCertTests(BaseRenewableCertTest): "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config) self.assertTrue(os.path.exists(os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.live_dir, "the-lineage.com-0001", "README"))) # Now trigger the detection of already existing files os.mkdir(os.path.join( self.cli_config.live_dir, "the-lineage.com-0002")) From 6eb3ce2f7a75c576a3c99c430d498bfd08a0f6e7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 10 Nov 2016 09:47:13 -0800 Subject: [PATCH 259/331] Bump python-cryptography to 1.5.3 (#3773) * [certbot-auto] Bump cryptography version to 1.5.2 * Actually bump to python-cryptography 1.5.3 * https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst#153---2016-11-05 Probably doesn't affect us, but best to ship the fix --- letsencrypt-auto-source/letsencrypt-auto | 55 ++++++++++--------- .../pieces/letsencrypt-auto-requirements.txt | 46 ++++++++-------- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 73b1b4ac8..72527f222 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -427,15 +427,20 @@ BootstrapGentooCommon() { app-misc/ca-certificates virtual/pkgconfig" + ASK_OPTION="--ask" + if [ "$ASSUME_YES" = 1 ]; then + ASK_OPTION="" + fi + case "$PACKAGE_MANAGER" in (paludis) $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x ;; (pkgcore) - $SUDO pmerge --noreplace --oneshot $PACKAGES + $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES ;; (portage|*) - $SUDO emerge --noreplace --oneshot $PACKAGES + $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES ;; esac } @@ -643,29 +648,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.5.2 \ - --hash=sha256:9e65f4c0ddcd4a7da3cfc1d87a0c3cf735a859c78f5f11d2346f7dfbc31df51b \ - --hash=sha256:a7e4a0f46ad767d4083faf31f4304301437f3919017203260620efbfeb72792b \ - --hash=sha256:0445831e72e38719e59b1f67b3361a0b43a52cb73ed10be757f6855310c75cb0 \ - --hash=sha256:4871e41cb3345c26b4596739a10c2dafa0a1207cef14ac9cdb923a55d0aa418b \ - --hash=sha256:eefe2e7f31833569d3ac9a4b796298f8d6deb0211c9c89e9f9ae7c774a6e982f \ - --hash=sha256:181f4f9943d95c144aad19264b31a3b7fa3a88a6d6a905fdfdfe985bf1ea0745 \ - --hash=sha256:3c73d538cb494924929a61376ecd7f802b28b12139546b9775d66ce8071efaf9 \ - --hash=sha256:713a68355550423dfb9167ed2365c1ad3aab1644cd7dcaf42afecc1e1d460dc5 \ - --hash=sha256:bf90be94e599ab3097f261ca606bad8c75b54eaeaaacfd0818fe6f2616ef9521 \ - --hash=sha256:446e9a139c87c09a07c9b252a2336bedffe99821f37332d4fc820d3da90d1738 \ - --hash=sha256:f04c00e81d42ec86e0b31a1d91783c3666691a85239f6daecfcda2cbe6c15f28 \ - --hash=sha256:5a7b9d10ef04e9cbbf0d9ba6a3502a1cc9176510e89522575d3e338f188eccfe \ - --hash=sha256:f2c35caf388e1f503b10c78ced239441f383c2b3d96e1b1c6fe04d56335af84e \ - --hash=sha256:f2b89bca3cd0efc7feb9aad0c8662508b4de3f26118808881868395ae32be337 \ - --hash=sha256:0194794e6bfc87a0fdb6e197a80ac5ea676e71bc2824281f9ccefb2be56ca2fb \ - --hash=sha256:44a968a20951481c9d8ffec4ba55326aa6d903c065568d0523600179ffd3976d \ - --hash=sha256:cd9a1734b0818ff57728affeb2444a2465a006e8a1eeee9d59ee4b5727a67ee7 \ - --hash=sha256:eb7f016eca0188bd310e2bd05b379ba378cd83b8962899e58f0f91feca607e47 \ - --hash=sha256:050caca01c8a67b4f1b10f2eca085c00154d1553a983d4ff7dfe2add9a270eaa \ - --hash=sha256:ef5692b5e44587e92b1231154bec5c7d0a262c75c4be754a6e605de8614145c6 \ - --hash=sha256:b634baf73c2b2f0e9c338434531aca3adffef47e78cba909da0ddcc9448f1c84 \ - --hash=sha256:eb8875736734e8e870b09be43b17f40472dc189b1c422a952fa8580768204832 +cryptography==1.5.3 \ + --hash=sha256:e514d92086246b53ae9b048df652cf3036b462e50a6ce9fac6b6253502679991 \ + --hash=sha256:10ee414f4b5af403a0d8f20dfa80f7dad1fc7ae5452ec5af03712d5b6e78c664 \ + --hash=sha256:7234456d1f4345a144ed07af2416c7c0659d4bb599dd1a963103dc8c183b370e \ + --hash=sha256:d3b9587406f94642bd70b3d666b813f446e95f84220c9e416ad94cbfb6be2eaa \ + --hash=sha256:b15fc6b59f1474eef62207c85888afada8acc47fae8198ba2b0197d54538961a \ + --hash=sha256:3b62d65d342704fc07ed171598db2a2775bdf587b1b6abd2cba2261bfe3ccde3 \ + --hash=sha256:059343022ec904c867a13bc55d2573e36c8cfb2c250e30d8a2e9825f253b07ba \ + --hash=sha256:c7897cf13bc8b4ee0215d83cbd51766d87c06b277fcca1f9108595508e5bcfb4 \ + --hash=sha256:9b69e983e5bf83039ddd52e52a28c7faedb2b22bdfb5876377b95aac7d3be63e \ + --hash=sha256:61e40905c426d02b3fae38088dc66ce4ef84830f7eb223dec6b3ac3ccdc676fb \ + --hash=sha256:00783a32bcd91a12177230d35bfcf70a2333ade4a6b607fac94a633a7971c671 \ + --hash=sha256:d11973f49b648cde1ea1a30e496d7557dbfeccd08b3cd9ba58d286a9c274ff8e \ + --hash=sha256:f24bedf28b81932ba6063aec9a826669f5237ea3b755efe04d98b072faa053a5 \ + --hash=sha256:3ab5725367239e3deb9b92e917aa965af3fef008f25b96a3000821869e208181 \ + --hash=sha256:8a53209de822e22b5f73bf4b99e68ac4ccc91051fd6751c8252982983e86a77d \ + --hash=sha256:5a07439d4b1e4197ac202b7eea45e26a6fd65757652dc50f1a63367f711df933 \ + --hash=sha256:26b1c4b40aec7b0074bceabe6e06565aa28176eca7323a31df66ebf89fe916d3 \ + --hash=sha256:eaa4a7b5a6682adcf8d6ebb2a08a008802657643655bb527c95c8a3860253d8e \ + --hash=sha256:8156927dcf8da274ff205ad0612f75c380df45385bacf98531a5b3348c88d135 \ + --hash=sha256:61ec0d792749d0e91e84b1d58b6dfd204806b10b5811f846c2ceca0de028c53a \ + --hash=sha256:26330c88041569ca621cc42274d0ea2667a48b6deab41467272c3aba0b6e8f07 \ + --hash=sha256:cf82ddac919b587f5e44247579b433224cc2e03332d2ea4d89aa70d7e6b64ae5 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 34f272e96..a6f3ea112 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -38,29 +38,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.5.2 \ - --hash=sha256:9e65f4c0ddcd4a7da3cfc1d87a0c3cf735a859c78f5f11d2346f7dfbc31df51b \ - --hash=sha256:a7e4a0f46ad767d4083faf31f4304301437f3919017203260620efbfeb72792b \ - --hash=sha256:0445831e72e38719e59b1f67b3361a0b43a52cb73ed10be757f6855310c75cb0 \ - --hash=sha256:4871e41cb3345c26b4596739a10c2dafa0a1207cef14ac9cdb923a55d0aa418b \ - --hash=sha256:eefe2e7f31833569d3ac9a4b796298f8d6deb0211c9c89e9f9ae7c774a6e982f \ - --hash=sha256:181f4f9943d95c144aad19264b31a3b7fa3a88a6d6a905fdfdfe985bf1ea0745 \ - --hash=sha256:3c73d538cb494924929a61376ecd7f802b28b12139546b9775d66ce8071efaf9 \ - --hash=sha256:713a68355550423dfb9167ed2365c1ad3aab1644cd7dcaf42afecc1e1d460dc5 \ - --hash=sha256:bf90be94e599ab3097f261ca606bad8c75b54eaeaaacfd0818fe6f2616ef9521 \ - --hash=sha256:446e9a139c87c09a07c9b252a2336bedffe99821f37332d4fc820d3da90d1738 \ - --hash=sha256:f04c00e81d42ec86e0b31a1d91783c3666691a85239f6daecfcda2cbe6c15f28 \ - --hash=sha256:5a7b9d10ef04e9cbbf0d9ba6a3502a1cc9176510e89522575d3e338f188eccfe \ - --hash=sha256:f2c35caf388e1f503b10c78ced239441f383c2b3d96e1b1c6fe04d56335af84e \ - --hash=sha256:f2b89bca3cd0efc7feb9aad0c8662508b4de3f26118808881868395ae32be337 \ - --hash=sha256:0194794e6bfc87a0fdb6e197a80ac5ea676e71bc2824281f9ccefb2be56ca2fb \ - --hash=sha256:44a968a20951481c9d8ffec4ba55326aa6d903c065568d0523600179ffd3976d \ - --hash=sha256:cd9a1734b0818ff57728affeb2444a2465a006e8a1eeee9d59ee4b5727a67ee7 \ - --hash=sha256:eb7f016eca0188bd310e2bd05b379ba378cd83b8962899e58f0f91feca607e47 \ - --hash=sha256:050caca01c8a67b4f1b10f2eca085c00154d1553a983d4ff7dfe2add9a270eaa \ - --hash=sha256:ef5692b5e44587e92b1231154bec5c7d0a262c75c4be754a6e605de8614145c6 \ - --hash=sha256:b634baf73c2b2f0e9c338434531aca3adffef47e78cba909da0ddcc9448f1c84 \ - --hash=sha256:eb8875736734e8e870b09be43b17f40472dc189b1c422a952fa8580768204832 +cryptography==1.5.3 \ + --hash=sha256:e514d92086246b53ae9b048df652cf3036b462e50a6ce9fac6b6253502679991 \ + --hash=sha256:10ee414f4b5af403a0d8f20dfa80f7dad1fc7ae5452ec5af03712d5b6e78c664 \ + --hash=sha256:7234456d1f4345a144ed07af2416c7c0659d4bb599dd1a963103dc8c183b370e \ + --hash=sha256:d3b9587406f94642bd70b3d666b813f446e95f84220c9e416ad94cbfb6be2eaa \ + --hash=sha256:b15fc6b59f1474eef62207c85888afada8acc47fae8198ba2b0197d54538961a \ + --hash=sha256:3b62d65d342704fc07ed171598db2a2775bdf587b1b6abd2cba2261bfe3ccde3 \ + --hash=sha256:059343022ec904c867a13bc55d2573e36c8cfb2c250e30d8a2e9825f253b07ba \ + --hash=sha256:c7897cf13bc8b4ee0215d83cbd51766d87c06b277fcca1f9108595508e5bcfb4 \ + --hash=sha256:9b69e983e5bf83039ddd52e52a28c7faedb2b22bdfb5876377b95aac7d3be63e \ + --hash=sha256:61e40905c426d02b3fae38088dc66ce4ef84830f7eb223dec6b3ac3ccdc676fb \ + --hash=sha256:00783a32bcd91a12177230d35bfcf70a2333ade4a6b607fac94a633a7971c671 \ + --hash=sha256:d11973f49b648cde1ea1a30e496d7557dbfeccd08b3cd9ba58d286a9c274ff8e \ + --hash=sha256:f24bedf28b81932ba6063aec9a826669f5237ea3b755efe04d98b072faa053a5 \ + --hash=sha256:3ab5725367239e3deb9b92e917aa965af3fef008f25b96a3000821869e208181 \ + --hash=sha256:8a53209de822e22b5f73bf4b99e68ac4ccc91051fd6751c8252982983e86a77d \ + --hash=sha256:5a07439d4b1e4197ac202b7eea45e26a6fd65757652dc50f1a63367f711df933 \ + --hash=sha256:26b1c4b40aec7b0074bceabe6e06565aa28176eca7323a31df66ebf89fe916d3 \ + --hash=sha256:eaa4a7b5a6682adcf8d6ebb2a08a008802657643655bb527c95c8a3860253d8e \ + --hash=sha256:8156927dcf8da274ff205ad0612f75c380df45385bacf98531a5b3348c88d135 \ + --hash=sha256:61ec0d792749d0e91e84b1d58b6dfd204806b10b5811f846c2ceca0de028c53a \ + --hash=sha256:26330c88041569ca621cc42274d0ea2667a48b6deab41467272c3aba0b6e8f07 \ + --hash=sha256:cf82ddac919b587f5e44247579b433224cc2e03332d2ea4d89aa70d7e6b64ae5 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 From 469fc3775f898854708adf7a4a683ceab62722ad Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Thu, 10 Nov 2016 23:15:17 +0200 Subject: [PATCH 260/331] Expanding tests for le-auto, adding Ubuntu test suite (#2548) * Adding Ubuntu Wily to LEA testing * Setting up certs correctly for Ubuntu 15.10 * Adding 12.04 * Removing redundant update-ca-certificates from 12.04 le-auto testing script * Fixing OpenSSL on Precise * Adding Vivid to le_auto tests * Cleaning up LEA tests configuration for Trusty * Ordering LEA test entries in .travis.yml and renaming them correctly * Removing Ubuntu Vivid * Refining comments * Removing Ubuntu Wily since it reached EOL * Removing .travis.yml duplicates * Fixing nits --- .travis.yml | 9 ++++-- letsencrypt-auto-source/Dockerfile.precise | 31 ++++++++++++++++++ .../{Dockerfile => Dockerfile.trusty} | 0 tox.ini | 32 ++++++++++++------- 4 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 letsencrypt-auto-source/Dockerfile.precise rename letsencrypt-auto-source/{Dockerfile => Dockerfile.trusty} (100%) diff --git a/.travis.yml b/.travis.yml index 733b45f52..ed58cff07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,18 +46,23 @@ matrix: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql services: docker + - sudo: required + env: TOXENV=apache_compat + services: docker + before_install: + addons: - sudo: required env: TOXENV=nginx_compat services: docker before_install: addons: - sudo: required - env: TOXENV=le_auto + env: TOXENV=le_auto_precise services: docker before_install: addons: - sudo: required - env: TOXENV=apache_compat + env: TOXENV=le_auto_trusty services: docker before_install: addons: diff --git a/letsencrypt-auto-source/Dockerfile.precise b/letsencrypt-auto-source/Dockerfile.precise new file mode 100644 index 000000000..c8b593774 --- /dev/null +++ b/letsencrypt-auto-source/Dockerfile.precise @@ -0,0 +1,31 @@ +# For running tests, build a docker image with a passwordless sudo and a trust +# store we can manipulate. + +FROM ubuntu:precise + +# Add an unprivileged user: +RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea + +# Install pip, sudo, openssl, and nose: +RUN apt-get update && \ + apt-get -q -y install python-pip sudo openssl && \ + apt-get clean +RUN pip install nose + +# Let that user sudo: +RUN sed -i.bkp -e \ + 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' \ + /etc/sudoers + +RUN mkdir -p /home/lea/certbot + +# Install fake testing CA: +COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/ + +# Copy code: +COPY . /home/lea/certbot/letsencrypt-auto-source + +USER lea +WORKDIR /home/lea + +CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/Dockerfile b/letsencrypt-auto-source/Dockerfile.trusty similarity index 100% rename from letsencrypt-auto-source/Dockerfile rename to letsencrypt-auto-source/Dockerfile.trusty diff --git a/tox.ini b/tox.ini index 7de8fb9bb..18fc252c8 100644 --- a/tox.ini +++ b/tox.ini @@ -99,17 +99,6 @@ commands = commands = {toxinidir}/tests/modification-check.sh -[testenv:le_auto] -# At the moment, this tests under Python 2.7 only, as only that version is -# readily available on the Trusty Docker image. -commands = - {toxinidir}/tests/modification-check.sh - docker build -t lea letsencrypt-auto-source - docker run --rm -t -i lea -whitelist_externals = - docker -passenv = DOCKER_* - [testenv:apache_compat] commands = docker build -t certbot-compatibility-test -f certbot-compatibility-test/Dockerfile . @@ -127,3 +116,24 @@ commands = whitelist_externals = docker passenv = DOCKER_* + +[testenv:le_auto_precise] +# At the moment, this tests under Python 2.7 only, as only that version is +# readily available on the Precise Docker image. +commands = + docker build -f letsencrypt-auto-source/Dockerfile.precise -t lea letsencrypt-auto-source + docker run --rm -t -i lea +whitelist_externals = + docker +passenv = DOCKER_* + +[testenv:le_auto_trusty] +# At the moment, this tests under Python 2.7 only, as only that version is +# readily available on the Trusty Docker image. +commands = + {toxinidir}/tests/modification-check.sh + docker build -f letsencrypt-auto-source/Dockerfile.trusty -t lea letsencrypt-auto-source + docker run --rm -t -i lea +whitelist_externals = + docker +passenv = DOCKER_* From c89bd421daf79de93b967a0785227984bd1c50f1 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 11 Nov 2016 00:01:15 +0200 Subject: [PATCH 261/331] Expanding tests for le-auto, adding Debian test suite (#2635) * Adding Debian 7 (Wheezy) to LE tests * Adding Debian 8 (Jessie) to LE tests * Fixing Debian Wheezy certificate addition error * Adding packages to LEA Debian Jessie test and refining the code commenting * Adding installing OpenSSL to the Debian Wheezy LEA test script * Removing LEA tests for Debian Jessie * Fixing nits --- .travis.yml | 5 ++++ letsencrypt-auto-source/Dockerfile.wheezy | 31 +++++++++++++++++++++++ tox.ini | 10 ++++++++ 3 files changed, 46 insertions(+) create mode 100644 letsencrypt-auto-source/Dockerfile.wheezy diff --git a/.travis.yml b/.travis.yml index ed58cff07..944c88471 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,6 +66,11 @@ matrix: services: docker before_install: addons: + - sudo: required + env: TOXENV=le_auto_wheezy + services: docker + before_install: + addons: - python: "2.7" env: TOXENV=apacheconftest sudo: required diff --git a/letsencrypt-auto-source/Dockerfile.wheezy b/letsencrypt-auto-source/Dockerfile.wheezy new file mode 100644 index 000000000..f86795e08 --- /dev/null +++ b/letsencrypt-auto-source/Dockerfile.wheezy @@ -0,0 +1,31 @@ +# For running tests, build a docker image with a passwordless sudo and a trust +# store we can manipulate. + +FROM debian:wheezy + +# Add an unprivileged user: +RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea + +# Install pip, sudo, openssl, and nose: +RUN apt-get update && \ + apt-get -q -y install python-pip sudo openssl && \ + apt-get clean +RUN pip install nose + +# Let that user sudo: +RUN sed -i.bkp -e \ + 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' \ + /etc/sudoers + +RUN mkdir -p /home/lea/certbot + +# Install fake testing CA: +COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/ + +# Copy code: +COPY . /home/lea/certbot/letsencrypt-auto-source + +USER lea +WORKDIR /home/lea + +CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/tox.ini b/tox.ini index 18fc252c8..426a1f707 100644 --- a/tox.ini +++ b/tox.ini @@ -137,3 +137,13 @@ commands = whitelist_externals = docker passenv = DOCKER_* + +[testenv:le_auto_wheezy] +# At the moment, this tests under Python 2.7 only, as only that version is +# readily available on the Wheezy Docker image. +commands = + docker build -f letsencrypt-auto-source/Dockerfile.wheezy -t lea letsencrypt-auto-source + docker run --rm -t -i lea +whitelist_externals = + docker +passenv = DOCKER_* From 1e27e43c148e524066d95fd55b618692e12290ad Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 11 Nov 2016 01:05:03 +0200 Subject: [PATCH 262/331] Expanding tests for le-auto, adding CentOS test suite (#2671) * Adding Dockerfile for CentOS 6 * Adding CentOS 7 to LEA tests * Enabling CentOS 6 LEA test * Removing CentOS 7 * Fixing nits * Using yum to install epel-release --- .travis.yml | 5 ++++ letsencrypt-auto-source/Dockerfile.centos6 | 32 ++++++++++++++++++++++ letsencrypt-auto-source/tests/auto_test.py | 22 +++++++-------- tox.ini | 10 +++++++ 4 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 letsencrypt-auto-source/Dockerfile.centos6 diff --git a/.travis.yml b/.travis.yml index 944c88471..a42e41352 100644 --- a/.travis.yml +++ b/.travis.yml @@ -71,6 +71,11 @@ matrix: services: docker before_install: addons: + - sudo: required + env: TOXENV=le_auto_centos6 + services: docker + before_install: + addons: - python: "2.7" env: TOXENV=apacheconftest sudo: required diff --git a/letsencrypt-auto-source/Dockerfile.centos6 b/letsencrypt-auto-source/Dockerfile.centos6 new file mode 100644 index 000000000..e1280109b --- /dev/null +++ b/letsencrypt-auto-source/Dockerfile.centos6 @@ -0,0 +1,32 @@ +# For running tests, build a docker image with a passwordless sudo and a trust +# store we can manipulate. + +FROM centos:6 + +RUN yum install -y epel-release + +# Install pip, sudo and nose: +RUN yum install -y python-pip sudo +RUN pip install nose + +# Add an unprivileged user: +RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups wheel --uid 1000 lea + +# Let that user sudo: +RUN sed -i.bkp -e \ + 's/# %wheel\(NOPASSWD: ALL\)\?/%wheel/g' \ + /etc/sudoers + +RUN mkdir -p /home/lea/certbot + +# Install fake testing CA: +COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/ +RUN update-ca-trust + +# Copy code: +COPY . /home/lea/certbot/letsencrypt-auto-source + +USER lea +WORKDIR /home/lea + +CMD ["nosetests", "-v", "-s", "certbot/letsencrypt-auto-source/tests"] diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 56023bc6f..6f21c28d5 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -279,8 +279,8 @@ class AutoTests(TestCase): ok_(re.match(r'letsencrypt \d+\.\d+\.\d+', err.strip().splitlines()[-1])) # Make a few assertions to test the validity of the next tests: - self.assertIn('Upgrading certbot-auto ', out) - self.assertIn('Creating virtual environment...', out) + self.assertTrue('Upgrading certbot-auto ' in out) + self.assertTrue('Creating virtual environment...' in out) # Now we have le-auto 99.9.9 and LE 99.9.9 installed. This # conveniently sets us up to test the next 2 cases. @@ -288,15 +288,15 @@ class AutoTests(TestCase): # Test when neither phase-1 upgrade nor phase-2 upgrade is # needed (probably a common case): out, err = run_letsencrypt_auto() - self.assertNotIn('Upgrading certbot-auto ', out) - self.assertNotIn('Creating virtual environment...', out) + self.assertFalse('Upgrading certbot-auto ' in out) + self.assertFalse('Creating virtual environment...' in out) # Test when a phase-1 upgrade is not needed but a phase-2 # upgrade is: set_le_script_version(venv_dir, '0.0.1') out, err = run_letsencrypt_auto() - self.assertNotIn('Upgrading certbot-auto ', out) - self.assertIn('Creating virtual environment...', out) + self.assertFalse('Upgrading certbot-auto ' in out) + self.assertTrue('Creating virtual environment...' in out) def test_openssl_failure(self): """Make sure we stop if the openssl signature check fails.""" @@ -313,9 +313,8 @@ class AutoTests(TestCase): out, err = run_le_auto(venv_dir, base_url) except CalledProcessError as exc: eq_(exc.returncode, 1) - self.assertIn("Couldn't verify signature of downloaded " - "certbot-auto.", - exc.output) + self.assertTrue("Couldn't verify signature of downloaded " + "certbot-auto." in exc.output) else: self.fail('Signature check on certbot-auto erroneously passed.') @@ -335,9 +334,8 @@ class AutoTests(TestCase): out, err = run_le_auto(venv_dir, base_url) except CalledProcessError as exc: eq_(exc.returncode, 1) - self.assertIn("THESE PACKAGES DO NOT MATCH THE HASHES " - "FROM THE REQUIREMENTS FILE", - exc.output) + self.assertTrue("THESE PACKAGES DO NOT MATCH THE HASHES " + "FROM THE REQUIREMENTS FILE" in exc.output) ok_(not exists(join(venv_dir, 'letsencrypt')), msg="The virtualenv was left around, even though " "installation didn't succeed. We shouldn't do " diff --git a/tox.ini b/tox.ini index 426a1f707..c5777dcd8 100644 --- a/tox.ini +++ b/tox.ini @@ -147,3 +147,13 @@ commands = whitelist_externals = docker passenv = DOCKER_* + +[testenv:le_auto_centos6] +# At the moment, this tests under Python 2.6 only, as only that version is +# readily available on the CentOS 6 Docker image. +commands = + docker build -f letsencrypt-auto-source/Dockerfile.centos6 -t lea letsencrypt-auto-source + docker run --rm -t -i lea +whitelist_externals = + docker +passenv = DOCKER_* From 1dd1afdc57fca2d3d6c7e31744f5da86aa92c08a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 10 Nov 2016 23:04:41 -0800 Subject: [PATCH 263/331] Remove letshelp-letsencrypt (#3775) --- letshelp-letsencrypt/LICENSE.txt | 190 ------------------ letshelp-letsencrypt/MANIFEST.in | 2 - letshelp-letsencrypt/README.rst | 2 - .../letshelp_letsencrypt/__init__.py | 8 - letshelp-letsencrypt/setup.py | 61 ------ 5 files changed, 263 deletions(-) delete mode 100644 letshelp-letsencrypt/LICENSE.txt delete mode 100644 letshelp-letsencrypt/MANIFEST.in delete mode 100644 letshelp-letsencrypt/README.rst delete mode 100644 letshelp-letsencrypt/letshelp_letsencrypt/__init__.py delete mode 100644 letshelp-letsencrypt/setup.py diff --git a/letshelp-letsencrypt/LICENSE.txt b/letshelp-letsencrypt/LICENSE.txt deleted file mode 100644 index 981c46c9f..000000000 --- a/letshelp-letsencrypt/LICENSE.txt +++ /dev/null @@ -1,190 +0,0 @@ - Copyright 2015 Electronic Frontier Foundation and others - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/letshelp-letsencrypt/MANIFEST.in b/letshelp-letsencrypt/MANIFEST.in deleted file mode 100644 index 97e2ad3df..000000000 --- a/letshelp-letsencrypt/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include LICENSE.txt -include README.rst diff --git a/letshelp-letsencrypt/README.rst b/letshelp-letsencrypt/README.rst deleted file mode 100644 index 57d0d8a3b..000000000 --- a/letshelp-letsencrypt/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -This package is a simple shim around the ``letshelp-certbot`` for backwards -compatibility. diff --git a/letshelp-letsencrypt/letshelp_letsencrypt/__init__.py b/letshelp-letsencrypt/letshelp_letsencrypt/__init__.py deleted file mode 100644 index fe4e272f9..000000000 --- a/letshelp-letsencrypt/letshelp_letsencrypt/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Tools for submitting server configurations.""" -import sys - - -import letshelp_certbot - - -sys.modules['letshelp_letsencrypt'] = letshelp_certbot diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py deleted file mode 100644 index 10380c49b..000000000 --- a/letshelp-letsencrypt/setup.py +++ /dev/null @@ -1,61 +0,0 @@ -import codecs -import os -import sys - -from setuptools import setup -from setuptools import find_packages - - -def read_file(filename, encoding='utf8'): - """Read unicode from given file.""" - with codecs.open(filename, encoding=encoding) as fd: - return fd.read() - - -here = os.path.abspath(os.path.dirname(__file__)) -readme = read_file(os.path.join(here, 'README.rst')) - - -version = '0.7.0.dev0' - - -# This package is a simple shim around letshelp-certbot -install_requires = ['letshelp-certbot'] - - -setup( - name='letshelp-letsencrypt', - version=version, - description="Let's help Let's Encrypt client", - long_description=readme, - url='https://github.com/letsencrypt/letsencrypt', - author="Certbot Project", - author_email='client-dev@letsencrypt.org', - license='Apache License 2.0', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Environment :: Plugins', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Security', - 'Topic :: System :: Installation/Setup', - 'Topic :: System :: Networking', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities', - ], - - packages=find_packages(), - include_package_data=True, - install_requires=install_requires, - entry_points={ - 'console_scripts': [ - 'letshelp-letsencrypt-apache = letshelp_certbot.apache:main', - ], - }, -) From 9aef15d09e79ee21466ade561356658db1c9d3a2 Mon Sep 17 00:00:00 2001 From: Henri Salo Date: Mon, 14 Nov 2016 20:58:14 +0200 Subject: [PATCH 264/331] Fix typo (#3790) --- docs/cli-help.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 5ff781aa3..cf93daa0e 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -225,7 +225,7 @@ certonly: server will still attempt to connect on port 443. (default: 443) --http-01-port HTTP01_PORT - Port used in the http-01 challenge.This only affects + Port used in the http-01 challenge. This only affects the port Certbot listens on. A conforming ACME server will still attempt to connect on port 80. (default: 80) From 3dbeef8ee74318a9d005ec1479e4d4687d2bc151 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 15 Nov 2016 11:45:07 -0800 Subject: [PATCH 265/331] fix --http-01-port typo at source (#3794) --- certbot/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 04843c694..8e7d887f0 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -234,7 +234,7 @@ class IConfig(zope.interface.Interface): "A conforming ACME server will still attempt to connect on port 443.") http01_port = zope.interface.Attribute( - "Port used in the http-01 challenge." + "Port used in the http-01 challenge. " "This only affects the port Certbot listens on. " "A conforming ACME server will still attempt to connect on port 80.") From e5f4d0cb5c91fa6db1f98757a87eb4f1c90f424f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 15 Nov 2016 11:56:05 -0800 Subject: [PATCH 266/331] Fix reinstall message (#3784) * Changed informational messages because of confusing message on reinstallation. Certbot prompts the user when it detects that an appropriately fresh certificate is already available: You have an existing certificate that contains exactly the same domains you requested and isn't close to expiry. (ref: ) What would you like to do? ------------------------------------------------------------------------------- 1: Attempt to reinstall this existing certificate 2: Renew & replace the cert (limit ~5 per 7 days) ------------------------------------------------------------------------------- Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1 On selecting '1' (reinstall), the resulting message is: ------------------------------------------------------------------------------- Your existing certificate has been successfully reinstalled, and the new certificate has been installed. The new certificate covers the following domains: https:// You should test your configuration at: https://www.ssllabs.com/ssltest/analyze.html?d= ------------------------------------------------------------------------------- "Your existing certificate has been successfully reinstalled" <-- Okay "and the new certificate has been installed." <-- Wait, what? The issue appears to come from assumptions in certbot/certbot/main.py It uses `len(lineage.available_versions("cert"))` to determine if this was a fresh install or renewal, and then calls either `display_ops.success_renewal()` (which produces the "existing certificate ... and the new certificate" language) or `display_ops.success_installation()` (which has no messaging about existing vs. new certificates). The len(lineage) test isn't the right way to make this choice. The certificate's lineage length doesn't imply anything about whether we've just obtained a new certificate, because there is no new certificate in the case of a "reinstall" action. The new logic calls `display_ops.success_installation()` on all "reinstall" actions, and otherwise employs the existing `len(lineage)` test. Additionally the `display_ops.success_installation()` has been enhanced to accept an action parameter, and has the message reworded slightly to make sense regardless of the action passed. The messaging is mostly unchanged if it's called without the action parameter: Original message: ------------------------------------------------------------------------------- Congratulations! You have successfully enabled https:// You should test your configuration at: https://www.ssllabs.com/ssltest/analyze.html?d= ------------------------------------------------------------------------------- New message on initial install: ------------------------------------------------------------------------------- Congratulations! You have successfully installed a certificate for https:// You should test your configuration at: https://www.ssllabs.com/ssltest/analyze.html?d= ------------------------------------------------------------------------------- New message on re-install: ------------------------------------------------------------------------------- Congratulations! You have successfully reinstalled a certificate for https:// You should test your configuration at: https://www.ssllabs.com/ssltest/analyze.html?d= ------------------------------------------------------------------------------- * Typo in display message. * Typo, characters transposed. * undo changes to certbot/display/ops.py * remove invalid todos * Test success_installation() called for reinstall * Simplify display_ops.success* functions * refactor and expand run() tests --- certbot/display/ops.py | 12 ++------ certbot/main.py | 4 +-- certbot/tests/display/ops_test.py | 2 +- certbot/tests/main_test.py | 50 +++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 6762c2244..97f050318 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -209,8 +209,6 @@ def _choose_names_manually(prompt_prefix=""): def success_installation(domains): """Display a box confirming the installation of HTTPS. - .. todo:: This should be centered on the screen - :param list domains: domain names which were enabled """ @@ -223,24 +221,20 @@ def success_installation(domains): pause=False) -def success_renewal(domains, action): +def success_renewal(domains): """Display a box confirming the renewal of an existing certificate. - .. todo:: This should be centered on the screen - :param list domains: domain names which were renewed - :param str action: can be "reinstall" or "renew" """ z_util(interfaces.IDisplay).notification( - "Your existing certificate has been successfully {3}ed, and the " + "Your existing certificate has been successfully renewed, and the " "new certificate has been installed.{1}{1}" "The new certificate covers the following domains: {0}{1}{1}" "You should test your configuration at:{1}{2}".format( _gen_https_names(domains), os.linesep, - os.linesep.join(_gen_ssl_lab_urls(domains)), - action), + os.linesep.join(_gen_ssl_lab_urls(domains))), pause=False) diff --git a/certbot/main.py b/certbot/main.py index 39c19bd7a..a84a77dc4 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -528,10 +528,10 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals le_client.enhance_config(domains, config, lineage.chain) - if len(lineage.available_versions("cert")) == 1: + if action in ("newcert", "reinstall",): display_ops.success_installation(domains) else: - display_ops.success_renewal(domains, action) + display_ops.success_renewal(domains) _suggest_donation_if_appropriate(config, action) diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 82ed325c8..ebb695024 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -321,7 +321,7 @@ class SuccessRenewalTest(unittest.TestCase): @classmethod def _call(cls, names): from certbot.display.ops import success_renewal - success_renewal(names, "renew") + success_renewal(names) @mock.patch("certbot.display.ops.z_util") def test_success_renewal(self, mock_util): diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 045a593e9..133606a19 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -13,9 +13,11 @@ from certbot import configuration from certbot import errors from certbot.plugins import disco as plugins_disco + class MainTest(unittest.TestCase): def setUp(self): pass + def tearDown(self): pass @@ -27,6 +29,54 @@ class MainTest(unittest.TestCase): ret = main._handle_identical_cert_request(mock.Mock(), mock_lineage) self.assertEqual(ret, ("reinstall", mock_lineage)) + +class RunTest(unittest.TestCase): + """Tests for certbot.main.run.""" + + def setUp(self): + self.domain = 'example.org' + self.patches = [ + mock.patch('certbot.main._auth_from_domains'), + mock.patch('certbot.main.display_ops.success_installation'), + mock.patch('certbot.main.display_ops.success_renewal'), + mock.patch('certbot.main._init_le_client'), + mock.patch('certbot.main._suggest_donation_if_appropriate')] + + self.mock_auth = self.patches[0].start() + self.mock_success_installation = self.patches[1].start() + self.mock_success_renewal = self.patches[2].start() + self.mock_init = self.patches[3].start() + self.mock_suggest_donation = self.patches[4].start() + + def tearDown(self): + for patch in self.patches: + patch.stop() + + def _call(self): + args = '-a webroot -i null -d {0}'.format(self.domain).split() + plugins = plugins_disco.PluginsRegistry.find_all() + config = configuration.NamespaceConfig( + cli.prepare_and_parse_args(plugins, args)) + + from certbot.main import run + run(config, plugins) + + def test_newcert_success(self): + self.mock_auth.return_value = ('newcert', mock.Mock()) + self._call() + self.mock_success_installation.assert_called_once_with([self.domain]) + + def test_reinstall_success(self): + self.mock_auth.return_value = ('reinstall', mock.Mock()) + self._call() + self.mock_success_installation.assert_called_once_with([self.domain]) + + def test_renewal_success(self): + self.mock_auth.return_value = ('renewal', mock.Mock()) + self._call() + self.mock_success_renewal.assert_called_once_with([self.domain]) + + class ObtainCertTest(unittest.TestCase): """Tests for certbot.main.obtain_cert.""" From 494c305b04738e2961b3e9c763d7ae976f00bf33 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Nov 2016 17:56:22 -0800 Subject: [PATCH 267/331] pin requests version in py26-oldest (#3803) --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index c5777dcd8..8e9e55b9d 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,8 @@ setenv = # cffi<=1.7 is required for oldest tests due to # https://bitbucket.org/cffi/cffi/commits/18cdf37d6b2691301a15b0e54f49757ebd4ed0f2?at=default +# requests<=2.11.1 required for py26-oldest tests due to +# https://github.com/shazow/urllib3/pull/930 deps = py{26,27}-oldest: cffi<=1.7 py{26,27}-oldest: cryptography==0.8 @@ -38,6 +40,7 @@ deps = py{26,27}-oldest: dnspython>=1.12 py{26,27}-oldest: psutil==2.1.0 py{26,27}-oldest: PyOpenSSL==0.13 + py26-oldest: requests<=2.11.1 [testenv:py33] commands = From 908e8a80a94c2abfaeeaedcfa17bde4056aac369 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 22 Nov 2016 19:18:32 +0100 Subject: [PATCH 268/331] disallow binary (wheel) install for pycparser (#3575) * disallow binary (wheel) install for pycparser pycparser has uploaded a broken wheel for 2.14, failing for two reasons 1. sha mismatch, due to not instructing pip which dist to install 2. bug in the wheel itself * regen letsencrypt-auto-source/letsencrypt-auto --- letsencrypt-auto-source/letsencrypt-auto | 3 ++- .../pieces/letsencrypt-auto-requirements.txt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 72527f222..35d484693 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -625,7 +625,8 @@ argparse==1.4.0 \ # This comes before cffi because cffi will otherwise install an unchecked # version via setup_requires. pycparser==2.14 \ - --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 + --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \ + --no-binary pycparser cffi==1.4.2 \ --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index a6f3ea112..cb0f891de 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -15,7 +15,8 @@ argparse==1.4.0 \ # This comes before cffi because cffi will otherwise install an unchecked # version via setup_requires. pycparser==2.14 \ - --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 + --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \ + --no-binary pycparser cffi==1.4.2 \ --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ From 7951ba73378db55a72305f0a35a125008ead078c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Nov 2016 14:00:48 -0800 Subject: [PATCH 269/331] pin pyopenssl 16.2.0 in certbot-auto (#3811) --- letsencrypt-auto-source/letsencrypt-auto | 6 +++--- .../pieces/letsencrypt-auto-requirements.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 35d484693..d041207dd 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -709,9 +709,9 @@ pyasn1==0.1.9 \ --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f -pyopenssl==16.0.0 \ - --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ - --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d +pyOpenSSL==16.2.0 \ + --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \ + --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e pyparsing==2.1.8 \ --hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \ --hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \ diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index cb0f891de..9bf06f0ef 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -99,9 +99,9 @@ pyasn1==0.1.9 \ --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f -pyopenssl==16.0.0 \ - --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ - --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d +pyOpenSSL==16.2.0 \ + --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \ + --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e pyparsing==2.1.8 \ --hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \ --hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \ From df5f08843feb3befb32762e5eae5b2e8fd2a8593 Mon Sep 17 00:00:00 2001 From: Craig Smith Date: Wed, 30 Nov 2016 09:00:37 +0930 Subject: [PATCH 270/331] Output success message for revoke command (#3823) * Output status for `revoke` operation. Fixes #2819. - Added method to `certbot.display.ops` to output confirmation of `revoke`. - Wrapped call to `acme.client.Client.revoke` in a try to statement to handle possible error. - Added test for `main.revoke`. * Added test for failure of certificate revocation. Moved creation of mocks into RevokeTest setup function. Stopped mocks in RevokeTest teardown function. * Fixed lint errors. * Do not call `unittest.TestCase.assertRaises` as a context manager (to work with py26). * Fixed spelling error in successful revocation notification. Added test for the notification. --- certbot/display/ops.py | 13 +++++++ certbot/main.py | 8 +++- certbot/tests/display/ops_test.py | 15 ++++++++ certbot/tests/main_test.py | 62 +++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 97f050318..ee7e750f6 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -237,6 +237,19 @@ def success_renewal(domains): os.linesep.join(_gen_ssl_lab_urls(domains))), pause=False) +def success_revocation(cert_path): + """Display a box confirming a certificate has been revoked. + + :param list cert_path: path to certificate which was revoked. + + """ + z_util(interfaces.IDisplay).notification( + "Congratulations! You have successfully revoked the certificate " + "that was located at {0}{1}{1}".format( + cert_path, + os.linesep), + pause=False) + def _gen_ssl_lab_urls(domains): """Returns a list of urls. diff --git a/certbot/main.py b/certbot/main.py index a84a77dc4..1f3379fd7 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -12,6 +12,7 @@ import zope.component from acme import jose from acme import messages +from acme import errors as acme_errors import certbot @@ -503,7 +504,12 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config key = acc.key acme = client.acme_from_config_key(config, key) cert = crypto_util.pyopenssl_load_certificate(config.cert_path[1])[0] - acme.revoke(jose.ComparableX509(cert)) + try: + acme.revoke(jose.ComparableX509(cert)) + except acme_errors.ClientError as e: + return e.message + + display_ops.success_revocation(config.cert_path[0]) def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index ebb695024..44efcf703 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -336,6 +336,21 @@ class SuccessRenewalTest(unittest.TestCase): for name in names: self.assertTrue(name in arg) +class SuccessRevocationTest(unittest.TestCase): + # pylint: disable=too-few-public-methods + """Test the success revocation message.""" + @classmethod + def _call(cls, path): + from certbot.display.ops import success_revocation + success_revocation(path) + + @mock.patch("certbot.display.ops.z_util") + def test_success_revocation(self, mock_util): + mock_util().notification.return_value = None + path = "/path/to/cert.pem" + self._call(path) + mock_util().notification.assert_called_once() + self.assertTrue(path in mock_util().notification.call_args[0][0]) if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 133606a19..ea744b570 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -3,6 +3,8 @@ import os import shutil import tempfile import unittest +import datetime +import pytz import mock @@ -13,6 +15,11 @@ from certbot import configuration from certbot import errors from certbot.plugins import disco as plugins_disco +from certbot.tests import test_util +from acme import jose + +CERT_PATH = test_util.vector_path('cert.pem') +KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem")) class MainTest(unittest.TestCase): def setUp(self): @@ -110,6 +117,61 @@ class ObtainCertTest(unittest.TestCase): # pylint: disable=unused-argument self.assertFalse(pause) +class RevokeTest(unittest.TestCase): + """Tests for certbot.main.revoke.""" + + def setUp(self): + self.tempdir_path = tempfile.mkdtemp() + shutil.copy(CERT_PATH, self.tempdir_path) + self.tmp_cert_path = os.path.abspath(os.path.join(self.tempdir_path, + 'cert.pem')) + + self.patches = [ + mock.patch('acme.client.Client'), + mock.patch('certbot.client.Client'), + mock.patch('certbot.main._determine_account'), + mock.patch('certbot.main.display_ops.success_revocation') + ] + self.mock_acme_client = self.patches[0].start() + self.patches[1].start() + self.mock_determine_account = self.patches[2].start() + self.mock_success_revoke = self.patches[3].start() + + from certbot.account import Account + + self.regr = mock.MagicMock() + self.meta = Account.Meta( + creation_host="test.certbot.org", + creation_dt=datetime.datetime( + 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC)) + self.acc = Account(self.regr, KEY, self.meta) + + self.mock_determine_account.return_value = (self.acc, None) + + + def tearDown(self): + shutil.rmtree(self.tempdir_path) + for patch in self.patches: + patch.stop() + + def _call(self): + args = 'revoke --cert-path={0}'.format(self.tmp_cert_path).split() + plugins = plugins_disco.PluginsRegistry.find_all() + config = configuration.NamespaceConfig( + cli.prepare_and_parse_args(plugins, args)) + + from certbot.main import revoke + revoke(config, plugins) + + def test_revocation_success(self): + self._call() + self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) + + def test_revocation_error(self): + from acme import errors as acme_errors + self.mock_acme_client.side_effect = acme_errors.ClientError() + self.assertRaises(acme_errors.ClientError, self._call) + self.mock_success_revoke.assert_not_called() class SetupLogFileHandlerTest(unittest.TestCase): """Tests for certbot.main.setup_log_file_handler.""" From 83966cdfcf97032ab026e4d99aa55f1fb5df44d2 Mon Sep 17 00:00:00 2001 From: Craig Smith Date: Wed, 30 Nov 2016 10:23:06 +0930 Subject: [PATCH 271/331] Fixed output (#3637). (#3809) --- certbot/cli.py | 1 + certbot/tests/cli_test.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index fa3bcc48b..d327240f4 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -335,6 +335,7 @@ class HelpfulArgumentParser(object): self.help_topics = HELP_TOPICS + plugin_names + [None] usage, short_usage = usage_strings(plugins) self.parser = configargparse.ArgParser( + prog="certbot", usage=short_usage, formatter_class=argparse.ArgumentDefaultsHelpFormatter, args_for_setting_config_path=["-c", "--config"], diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 8d4d0af62..54ae74f95 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -143,6 +143,21 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods out = self._help_output(['-h']) self.assertTrue(cli.usage_strings(plugins)[0] in out) + def test_version_string_program_name(self): + toy_out = six.StringIO() + toy_err = six.StringIO() + with mock.patch('certbot.main.sys.stdout', new=toy_out): + with mock.patch('certbot.main.sys.stderr', new=toy_err): + try: + main.main(["--version"]) + except SystemExit: + pass + finally: + output = toy_out.getvalue() or toy_err.getvalue() + self.assertTrue("certbot" in output, "Output is {0}".format(output)) + toy_out.close() + toy_err.close() + def _cli_missing_flag(self, args, message): "Ensure that a particular error raises a missing cli flag error containing message" exc = None From 0956e61c7c8653218bcaa46087d4508fc795feaa Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Nov 2016 10:47:10 -0800 Subject: [PATCH 272/331] Remove the sphinxcontrib.programout [docs]dependency (#3830) - By making a static text file at release time, and including that instead. --- acme/docs/conf.py | 1 - acme/docs/jws-help.txt | 8 ++++++++ acme/docs/man/jws.rst | 2 +- acme/setup.py | 1 - docs/api/log.rst | 5 ----- docs/conf.py | 1 - setup.py | 1 - tools/release.sh | 1 + 8 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 acme/docs/jws-help.txt delete mode 100644 docs/api/log.rst diff --git a/acme/docs/conf.py b/acme/docs/conf.py index 55f5eee3f..dea23a8ca 100644 --- a/acme/docs/conf.py +++ b/acme/docs/conf.py @@ -39,7 +39,6 @@ extensions = [ 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', - 'sphinxcontrib.programoutput', ] autodoc_member_order = 'bysource' diff --git a/acme/docs/jws-help.txt b/acme/docs/jws-help.txt new file mode 100644 index 000000000..34cf5ce23 --- /dev/null +++ b/acme/docs/jws-help.txt @@ -0,0 +1,8 @@ +usage: jws [-h] [--compact] {sign,verify} ... + +positional arguments: + {sign,verify} + +optional arguments: + -h, --help show this help message and exit + --compact diff --git a/acme/docs/man/jws.rst b/acme/docs/man/jws.rst index fb3121a96..d7ff8f405 100644 --- a/acme/docs/man/jws.rst +++ b/acme/docs/man/jws.rst @@ -1 +1 @@ -.. program-output:: jws --help all +.. literalinclude:: ../jws-help.txt diff --git a/acme/setup.py b/acme/setup.py index 2b32f7e28..895889c7c 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -49,7 +49,6 @@ dev_extras = [ docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', - 'sphinxcontrib-programoutput', ] diff --git a/docs/api/log.rst b/docs/api/log.rst deleted file mode 100644 index 41311de90..000000000 --- a/docs/api/log.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.log` ----------------------- - -.. automodule:: certbot.log - :members: diff --git a/docs/conf.py b/docs/conf.py index e387e1eae..7b3f2026c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,7 +45,6 @@ extensions = [ 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'repoze.sphinx.autointerface', - 'sphinxcontrib.programoutput', ] autodoc_member_order = 'bysource' diff --git a/setup.py b/setup.py index 90c98d469..46dbdac81 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,6 @@ docs_extras = [ 'repoze.sphinx.autointerface', 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', - 'sphinxcontrib-programoutput', ] setup( diff --git a/tools/release.sh b/tools/release.sh index c4bb61f2f..be306d8e0 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -147,6 +147,7 @@ cd ~- # get a snapshot of the CLI help for the docs certbot --help all > docs/cli-help.txt +jws --help > acme/docs/jws-help.txt cd .. # freeze before installing anything else, so that we know end-user KGS From ec0cd4d5383203c440d6528e1308c5e2281e5300 Mon Sep 17 00:00:00 2001 From: Mario Villaplana Date: Wed, 30 Nov 2016 13:50:16 -0500 Subject: [PATCH 273/331] Warn early if a selected enhancement is unsupported by the current plugin (#3688) Certbot currently silently allows a user to specify enhancements that are unsupported by the chosen plugin. This adds an early warning message indicating when a selected enhancement isn't supported by a plugin. --- certbot/client.py | 34 +++++++++++++++++++++++++++++++++- certbot/tests/client_test.py | 11 +++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 880cfe7df..b9dbaf480 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -196,6 +196,11 @@ class Client(object): else: self.auth_handler = None + # Warn if the client is using unsupported config options with an + # installer + if self.installer is not None: + self._verify_all_config_options_supported(config) + def obtain_certificate_from_csr(self, domains, csr, typ=OpenSSL.crypto.FILETYPE_ASN1, authzr=None): """Obtain certificate. @@ -411,9 +416,10 @@ class Client(object): raise errors.Error("No config available") supported = self.installer.supported_enhancements() + redirect = config.redirect if "redirect" in supported else False hsts = config.hsts if "ensure-http-header" in supported else False - uir = config.uir if "ensure-http-header" in supported else False + uir = config.uir if "ensure-http-header" in supported else False staple = config.staple if "staple-ocsp" in supported else False if redirect is None: @@ -502,6 +508,32 @@ class Client(object): raise reporter.add_message(success_msg, reporter.HIGH_PRIORITY) + def _verify_all_config_options_supported(self, config): + """Verifies that all config options are supported in current installer. + + :ivar config: Namespace typically produced by + :meth:`argparse.ArgumentParser.parse_args`. + :type namespace: :class:`argparse.Namespace` + """ + # Mapping between config options and string describing config option + # support in installer supported_enhancements. + option_support = { + "redirect": "redirect", + "hsts": "ensure-http-header", + "uir": "ensure-http-header", + "staple": "staple-ocsp" + } + + supported = self.installer.supported_enhancements() + + for config_name, support_string in option_support.items(): + config_value = getattr(config, config_name, None) + if config_value and support_string not in supported: + msg = ("Option %s is not allowed with the current installer. " + "Please disable the option and try again." % + config_name) + logger.warning(msg) + def validate_key_csr(privkey, csr=None): """Validate Key and CSR files. diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index d61025116..02e0bedf9 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -120,6 +120,17 @@ class ClientTest(unittest.TestCase): config=self.config, account_=self.account, auth=None, installer=None) + def test__init___warn_unsupported_config_options(self): + config = ConfigHelper(redirect=True, hsts=True) + installer = mock.MagicMock() + installer.supported_enhancements.return_value = ["redirect"] + + with mock.patch('certbot.client.logger') as mock_logger: + from certbot.client import Client + Client(config=config, account_=self.account, auth=None, + installer=installer, acme=self.acme) + mock_logger.warning.assert_called_once_with(mock.ANY) + def test_init_acme_verify_ssl(self): net = self.acme_client.call_args[1]["net"] self.assertTrue(net.verify_ssl) From 0289457a931984b83826be3068559287aa14c4cb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 30 Nov 2016 16:09:16 -0800 Subject: [PATCH 274/331] Use ${foo+x} not ${foo:+x} (#3833) --- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-auto-source/letsencrypt-auto.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index d041207dd..a5affb381 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -110,7 +110,7 @@ su_sudo() { SUDO_ENV="" export CERTBOT_AUTO="$0" -if [ -n "${LE_AUTO_SUDO:+x}" ]; then +if [ -n "${LE_AUTO_SUDO+x}" ]; then case "$LE_AUTO_SUDO" in su_sudo|su) SUDO=su_sudo diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 175325d40..619306979 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -110,7 +110,7 @@ su_sudo() { SUDO_ENV="" export CERTBOT_AUTO="$0" -if [ -n "${LE_AUTO_SUDO:+x}" ]; then +if [ -n "${LE_AUTO_SUDO+x}" ]; then case "$LE_AUTO_SUDO" in su_sudo|su) SUDO=su_sudo From edbb3a73c6085219bef0cbf008bd1c82088bfcf6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 1 Dec 2016 10:47:08 -0800 Subject: [PATCH 275/331] Take advantage of urllib3 pyopenssl rewrite (#3805) * pin requests version in py26-oldest * Determine requests security deps dynamically Starting with requests 2.12, pyasn1 and ndg-httpsclient are no longer needed to inject pyopenssl into urllib3. This change allows us to determine whether or not these dependencies are required at install time. If an older version of requests is used, these packages are still installed. If a new version of requests is used, they are not reducing the number of dependencies we have. * Bump requests version in certbot-auto * Use pkg_resources in activate test Due to pip's lack of dependency resolution, the change to use requests[extras] causes errors in acme.util_test because pkg_resources accurately detects the "missing" dependency. There isn't a real problem here. The problem comes from a brand new requests and ancient pyopenssl as well as a unit test for functionality we plan to remove in our next release. I modified the unit test to fix the problem for now. * Use six instead of pkg_resources for test * Require requests<=2.11.1 in py27-oldest test If we don't do this, we get test failures for the certbot package which is actually a good thing! pkg_resources is catching the unlikely but possible problem I describe in #3803 and erroring out saying it is missing the necessary dependencies to run certbot. Good job package resources. * Undo changes to acme.util_test --- acme/setup.py | 4 +--- letsencrypt-auto-source/letsencrypt-auto | 20 +++---------------- .../pieces/letsencrypt-auto-requirements.txt | 20 +++---------------- tox.ini | 4 ++-- 4 files changed, 9 insertions(+), 39 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 895889c7c..5524a6734 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -11,13 +11,11 @@ install_requires = [ # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) 'cryptography>=0.8', - 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) - 'pyasn1', # urllib3 InsecurePlatformWarning (#304) # Connection.set_tlsext_host_name (>=0.13) 'PyOpenSSL>=0.13', 'pyrfc3339', 'pytz', - 'requests', + 'requests[security]>=2.4.1', # security extras added in 2.4.1 # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: 'setuptools>=1.0', diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index a5affb381..5c1efc9fe 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -687,8 +687,6 @@ ipaddress==1.0.16 \ linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c -ndg-httpsclient==0.4.0 \ - --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274 ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f parsedatetime==2.1 \ @@ -697,18 +695,6 @@ parsedatetime==2.1 \ pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 -pyasn1==0.1.9 \ - --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ - --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ - --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ - --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ - --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ - --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ - --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ - --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ - --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ - --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ - --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f pyOpenSSL==16.2.0 \ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e @@ -740,9 +726,9 @@ pytz==2015.7 \ --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 -requests==2.9.1 \ - --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \ - --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f +requests==2.12.1 \ + --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \ + --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e six==1.10.0 \ --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 9bf06f0ef..a00f0bbb9 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -77,8 +77,6 @@ ipaddress==1.0.16 \ linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c -ndg-httpsclient==0.4.0 \ - --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274 ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f parsedatetime==2.1 \ @@ -87,18 +85,6 @@ parsedatetime==2.1 \ pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 -pyasn1==0.1.9 \ - --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ - --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ - --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ - --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ - --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ - --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ - --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ - --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ - --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ - --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ - --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f pyOpenSSL==16.2.0 \ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e @@ -130,9 +116,9 @@ pytz==2015.7 \ --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 -requests==2.9.1 \ - --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \ - --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f +requests==2.12.1 \ + --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \ + --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e six==1.10.0 \ --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a diff --git a/tox.ini b/tox.ini index 8e9e55b9d..a6a017efe 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ setenv = # cffi<=1.7 is required for oldest tests due to # https://bitbucket.org/cffi/cffi/commits/18cdf37d6b2691301a15b0e54f49757ebd4ed0f2?at=default -# requests<=2.11.1 required for py26-oldest tests due to +# requests<=2.11.1 required for oldest tests due to # https://github.com/shazow/urllib3/pull/930 deps = py{26,27}-oldest: cffi<=1.7 @@ -40,7 +40,7 @@ deps = py{26,27}-oldest: dnspython>=1.12 py{26,27}-oldest: psutil==2.1.0 py{26,27}-oldest: PyOpenSSL==0.13 - py26-oldest: requests<=2.11.1 + py{26,27}-oldest: requests<=2.11.1 [testenv:py33] commands = From 8b67a58f3c9bf81f94d49357bd68421145840051 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 2 Dec 2016 15:11:35 -0800 Subject: [PATCH 276/331] Ensure tests pass with openssl 1.1 (#3827) * Ensure tests pass with openssl 1.1 A bunch of the acme.standalone and acme.crypto_util tests were using weak crypto that is now prohibited :/ * lint * lintlint * Fix symlink --- acme/acme/crypto_util.py | 4 ++ acme/acme/crypto_util_test.py | 4 +- acme/acme/standalone_test.py | 12 +++-- acme/acme/testdata/rsa2048_cert.pem | 22 +++++++++ acme/acme/testdata/rsa2048_key.pem | 55 +++++++++++---------- acme/examples/standalone/localhost/cert.pem | 2 +- acme/examples/standalone/localhost/key.pem | 2 +- 7 files changed, 65 insertions(+), 36 deletions(-) create mode 100644 acme/acme/testdata/rsa2048_cert.pem diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 2b2133475..266f2c0c7 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -63,6 +63,8 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods server_name) return new_context = OpenSSL.SSL.Context(self.method) + new_context.set_options(OpenSSL.SSL.OP_NO_SSLv2) + new_context.set_options(OpenSSL.SSL.OP_NO_SSLv3) new_context.use_privatekey(key) new_context.use_certificate(cert) connection.set_context(new_context) @@ -86,6 +88,8 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods sock, addr = self.sock.accept() context = OpenSSL.SSL.Context(self.method) + context.set_options(OpenSSL.SSL.OP_NO_SSLv2) + context.set_options(OpenSSL.SSL.OP_NO_SSLv3) context.set_tlsext_servername_callback(self._pick_certificate_cb) ssl_sock = self.FakeConnection(OpenSSL.SSL.Connection(context, sock)) diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 75a908d4f..913b50164 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -19,8 +19,8 @@ class SSLSocketAndProbeSNITest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket/probe_sni.""" def setUp(self): - self.cert = test_util.load_comparable_cert('cert.pem') - key = test_util.load_pyopenssl_private_key('rsa512_key.pem') + self.cert = test_util.load_comparable_cert('rsa2048_cert.pem') + key = test_util.load_pyopenssl_private_key('rsa2048_key.pem') # pylint: disable=protected-access certs = {b'foo': (key, self.cert.wrapped)} diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 85cd9d11d..92a0b4272 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -33,8 +33,8 @@ class TLSSNI01ServerTest(unittest.TestCase): def setUp(self): self.certs = {b'localhost': ( - test_util.load_pyopenssl_private_key('rsa512_key.pem'), - test_util.load_cert('cert.pem'), + test_util.load_pyopenssl_private_key('rsa2048_key.pem'), + test_util.load_cert('rsa2048_cert.pem'), )} from acme.standalone import TLSSNI01Server self.server = TLSSNI01Server(("", 0), certs=self.certs) @@ -114,8 +114,9 @@ class TestSimpleTLSSNI01Server(unittest.TestCase): self.test_cwd = tempfile.mkdtemp() localhost_dir = os.path.join(self.test_cwd, 'localhost') os.makedirs(localhost_dir) - shutil.copy(test_util.vector_path('cert.pem'), localhost_dir) - shutil.copy(test_util.vector_path('rsa512_key.pem'), + shutil.copy(test_util.vector_path('rsa2048_cert.pem'), + os.path.join(localhost_dir, 'cert.pem')) + shutil.copy(test_util.vector_path('rsa2048_key.pem'), os.path.join(localhost_dir, 'key.pem')) from acme.standalone import simple_tls_sni_01_server @@ -147,7 +148,8 @@ class TestSimpleTLSSNI01Server(unittest.TestCase): time.sleep(1) # wait until thread starts else: self.assertEqual(jose.ComparableX509(cert), - test_util.load_comparable_cert('cert.pem')) + test_util.load_comparable_cert( + 'rsa2048_cert.pem')) break diff --git a/acme/acme/testdata/rsa2048_cert.pem b/acme/acme/testdata/rsa2048_cert.pem new file mode 100644 index 000000000..3944cd1db --- /dev/null +++ b/acme/acme/testdata/rsa2048_cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV +BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do +ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx +MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC +V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy +dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN +PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm +7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn +xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD +g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1 +RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU +uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T +RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx +nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8 +kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+ +aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P +AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf +Esg= +-----END CERTIFICATE----- diff --git a/acme/acme/testdata/rsa2048_key.pem b/acme/acme/testdata/rsa2048_key.pem index 33efd3467..5847aed55 100644 --- a/acme/acme/testdata/rsa2048_key.pem +++ b/acme/acme/testdata/rsa2048_key.pem @@ -1,27 +1,28 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA8HwZMHeImB/iM8/n8CTCR4KeYQB2gLGO3v8xLms+PWH3Zbxc -dVtEn25Y34scIh+iOuEXBcSBalBddLHKBGVN3nCfmpupoLm52xgRG44q9OWODpg4 -FSi4afqVw2agMx0RHi0v3GVcdpqB83UW42kK1ESZHUuq7mxLg8u3IMYZFm6Amsf+ -YQjBbDNn8NczJOFhsExP2EdM5ykgM1Om8aqTqqPMgPub68/r4Sym+BjLnvRq5Qtz -h/jCfOBIIpAwg3lj7l8OyE3kkD3ALtuiuminNUqLHEkUaLq/Xiv8V8mvnrhG7h3Q -+L1Xc707P0dz5YM5XxTMhmUE1cae/lQ0KbNrpwIDAQABAoIBAAiDXCDrGlrIRimv -YnaN1pLRfOnSKl/D6VrbjdIm2b0yip9/W4aMBJHgRiUjt4s9s3CCJ1585lftIGHR -KWWecHM/aWb/u7GE4Z9v6qsfDUY+GhlKKjIVjvGxfTu9lk446TI4R0l2DR/luFP2 -ASlrvoZlJ0ZyN0rZapLv0zvFx32Tukd+3rcMmXfHl7aRGMZG1YTKNmBJ4d9iJ6cP -HG3fgSzLQMPLNO/20MzbXdREG5FNQtwaMuFnIcVbtMCvc/71lQQEfANMLCUweEed -YWGOjgDeh+731nJsopel+2TSTgnf5VhcFrgChZZdqeKvP+HbXjTE2VkWo7BrzoM7 -xICYBwECgYEA/ZF/JOjZfwIMUdikv8vldJzRMdFryl4NJWnh4NeThNOEpUcpxnyK -wyMnnQaGJa51u9EEnzl0sZ2h2ODjD6KFpz6fkWaVRq5SWalVPAoKZGaoPZV3IUOI -8Tm0xkXho+A/FUUEcxCLME+3V9EdPfHaVRJOrbfDyxvNhsj4w9F0aAkCgYEA8sp7 -XTrolOknJGv4Qt1w6gcm5+cMtLaRfi8ZHPHujl2x9eWE8/s2818az7jc0Xr/G4HQ -NeU+3Es4BblEckSHmhUZhx26cZgkLSIIDofEtaEc6u8CyWfxsWvn3l4T3kMdeSLC -9UoLk59AH2tkMIh8vzV8LSisLJa341lMdgryQi8CgYAlJKr7PSCe+i3Tz2hSsAts -iYwbQBIKErzaPihYRzvUuSc1DreP26535y5mUg5UdrnISVXj/Qaa/fw3SLn6EFSD -qyi0o9I6CE8H00YpBU+AZYk/fCV3Oe1VaJ6SbKog1zhmZTXBpSq+aO7ybi9aY5MX -4xajW8fSeMAifk3yYTwsAQKBgErcEcOCOVpItU/uloKPYpRWFjHktK83p46fmP+q -vOJak1d9KExOBfhuN4caucNBSE1D7l3fzE0CSEjDgg41gRYKMW/Ow8DopybfWlqY -lBdokNEDVvmgug35dmnC2h9q1DiYdkJJTV57+Lp3U1H/k28lX59Q7h1lb1eDHic7 -YszzAoGBAOx05dhOiYbzAJSTQu3oBHFn4mTYIqCcDO6cQrEJwPKAq7mAhT0yOk9N -CrqRV/1aes665829cyTwcAZl6nqbzHv5XjX5+g6vmooCb4oCkq49rumHjoQdrX8D -RR5b+Spkc1jo4rctCcExzSkgo+K5N3oBVYznecje7O7Z0/qiJE/8 ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq +r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo +VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW +AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ +MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W +Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp +v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3 +3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO +z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB +o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK +lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH +Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a +hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/ +IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/ +9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP +n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj +9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy +ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb +f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3 +YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3 +xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG +wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL +Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK +jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh +QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6 +kmf1UgGFcKrJuXgwEtTVxw== +-----END PRIVATE KEY----- diff --git a/acme/examples/standalone/localhost/cert.pem b/acme/examples/standalone/localhost/cert.pem index 569366af9..1cca87af5 120000 --- a/acme/examples/standalone/localhost/cert.pem +++ b/acme/examples/standalone/localhost/cert.pem @@ -1 +1 @@ -../../../acme/testdata/cert.pem \ No newline at end of file +../../../acme/testdata/rsa2048_cert.pem \ No newline at end of file diff --git a/acme/examples/standalone/localhost/key.pem b/acme/examples/standalone/localhost/key.pem index 870f4f876..ee3dd2847 120000 --- a/acme/examples/standalone/localhost/key.pem +++ b/acme/examples/standalone/localhost/key.pem @@ -1 +1 @@ -../../../acme/testdata/rsa512_key.pem \ No newline at end of file +../../../acme/testdata/rsa2048_key.pem \ No newline at end of file From da3332ccfaad08bd9dece68f5ae71308acb1d776 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 2 Dec 2016 16:03:55 -0800 Subject: [PATCH 277/331] Security enhancement cleanup (#3837) * Stop passing around config and refactor tests * Refactor and warn during enhance_config * Use mock.ANY to make new Pythons happy * Remove verbose enhance_config from test names * Fix spacing in warning --- certbot/client.py | 83 +++--------- certbot/main.py | 4 +- certbot/tests/client_test.py | 243 +++++++++++++++-------------------- 3 files changed, 123 insertions(+), 207 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index b9dbaf480..4d6de6375 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -196,11 +196,6 @@ class Client(object): else: self.auth_handler = None - # Warn if the client is using unsupported config options with an - # installer - if self.installer is not None: - self._verify_all_config_options_supported(config) - def obtain_certificate_from_csr(self, domains, csr, typ=OpenSSL.crypto.FILETYPE_ASN1, authzr=None): """Obtain certificate. @@ -388,16 +383,10 @@ class Client(object): # sites may have been enabled / final cleanup self.installer.restart() - def enhance_config(self, domains, config, chain_path): + def enhance_config(self, domains, chain_path): """Enhance the configuration. :param list domains: list of domains to configure - - :ivar config: Namespace typically produced by - :meth:`argparse.ArgumentParser.parse_args`. - it must have the redirect, hsts and uir attributes. - :type namespace: :class:`argparse.Namespace` - :param chain_path: chain file path :type chain_path: `str` or `None` @@ -405,40 +394,34 @@ class Client(object): client. """ - if self.installer is None: logger.warning("No installer is specified, there isn't any " "configuration to enhance.") raise errors.Error("No installer available") - if config is None: - logger.warning("No config is specified.") - raise errors.Error("No config available") - + enhanced = False + enhancement_info = ( + ("hsts", "ensure-http-header", "Strict-Transport-Security"), + ("redirect", "redirect", None), + ("staple", "staple-ocsp", chain_path), + ("uir", "ensure-http-header", "Upgrade-Insecure-Requests"),) supported = self.installer.supported_enhancements() - redirect = config.redirect if "redirect" in supported else False - hsts = config.hsts if "ensure-http-header" in supported else False - uir = config.uir if "ensure-http-header" in supported else False - staple = config.staple if "staple-ocsp" in supported else False - - if redirect is None: - redirect = enhancements.ask("redirect") - - if redirect: - self.apply_enhancement(domains, "redirect") - - if hsts: - self.apply_enhancement(domains, "ensure-http-header", - "Strict-Transport-Security") - if uir: - self.apply_enhancement(domains, "ensure-http-header", - "Upgrade-Insecure-Requests") - if staple: - self.apply_enhancement(domains, "staple-ocsp", chain_path) + for config_name, enhancement_name, option in enhancement_info: + config_value = getattr(self.config, config_name) + if enhancement_name in supported: + if config_name == "redirect" and config_value is None: + config_value = enhancements.ask(enhancement_name) + if config_value: + self.apply_enhancement(domains, enhancement_name, option) + enhanced = True + elif config_value: + logger.warning( + "Option %s is not supported by the selected installer. " + "Skipping enhancement.", config_name) msg = ("We were unable to restart web server") - if redirect or hsts or uir or staple: + if enhanced: with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart() @@ -508,32 +491,6 @@ class Client(object): raise reporter.add_message(success_msg, reporter.HIGH_PRIORITY) - def _verify_all_config_options_supported(self, config): - """Verifies that all config options are supported in current installer. - - :ivar config: Namespace typically produced by - :meth:`argparse.ArgumentParser.parse_args`. - :type namespace: :class:`argparse.Namespace` - """ - # Mapping between config options and string describing config option - # support in installer supported_enhancements. - option_support = { - "redirect": "redirect", - "hsts": "ensure-http-header", - "uir": "ensure-http-header", - "staple": "staple-ocsp" - } - - supported = self.installer.supported_enhancements() - - for config_name, support_string in option_support.items(): - config_value = getattr(config, config_name, None) - if config_value and support_string not in supported: - msg = ("Option %s is not allowed with the current installer. " - "Please disable the option and try again." % - config_name) - logger.warning(msg) - def validate_key_csr(privkey, csr=None): """Validate Key and CSR files. diff --git a/certbot/main.py b/certbot/main.py index 1f3379fd7..37af009e3 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -435,7 +435,7 @@ def install(config, plugins): le_client.deploy_certificate( domains, config.key_path, config.cert_path, config.chain_path, config.fullchain_path) - le_client.enhance_config(domains, config, config.chain_path) + le_client.enhance_config(domains, config.chain_path) def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print @@ -532,7 +532,7 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals domains, lineage.privkey, lineage.cert, lineage.chain, lineage.fullchain) - le_client.enhance_config(domains, config, lineage.chain) + le_client.enhance_config(domains, lineage.chain) if action in ("newcert", "reinstall",): display_ops.success_installation(domains) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 02e0bedf9..d79fb1852 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -102,15 +102,13 @@ class RegisterTest(unittest.TestCase): mock_client().register.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(messages.Error, self._call) -class ClientTest(unittest.TestCase): - """Tests for certbot.client.Client.""" +class ClientTestCommon(unittest.TestCase): + """Common base class for certbot.client.Client tests.""" def setUp(self): - self.config = mock.MagicMock( - no_verify_ssl=False, config_dir="/etc/letsencrypt", allow_subset_of_names=False) # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) - self.eg_domains = ["example.com", "www.example.com"] + self.config = mock.MagicMock(no_verify_ssl=False) from certbot.client import Client with mock.patch("certbot.client.acme_client.Client") as acme: @@ -120,16 +118,15 @@ class ClientTest(unittest.TestCase): config=self.config, account_=self.account, auth=None, installer=None) - def test__init___warn_unsupported_config_options(self): - config = ConfigHelper(redirect=True, hsts=True) - installer = mock.MagicMock() - installer.supported_enhancements.return_value = ["redirect"] - with mock.patch('certbot.client.logger') as mock_logger: - from certbot.client import Client - Client(config=config, account_=self.account, auth=None, - installer=installer, acme=self.acme) - mock_logger.warning.assert_called_once_with(mock.ANY) +class ClientTest(ClientTestCommon): + """Tests for certbot.client.Client.""" + def setUp(self): + super(ClientTest, self).setUp() + + self.config.allow_subset_of_names = False + self.config.config_dir = "/etc/letsencrypt" + self.eg_domains = ["example.com", "www.example.com"] def test_init_acme_verify_ssl(self): net = self.acme_client.call_args[1]["net"] @@ -325,147 +322,109 @@ class ClientTest(unittest.TestCase): installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) - @mock.patch("certbot.client.enhancements") - def test_enhance_config(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.Error, self.client.enhance_config, - ["foo.bar"], config, None) - mock_enhancements.ask.return_value = True - installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = ["redirect"] +class EnhanceConfigTest(ClientTestCommon): + """Tests for certbot.client.Client.enhance_config.""" + def setUp(self): + super(EnhanceConfigTest, self).setUp() - self.client.enhance_config(["foo.bar"], config, None) - installer.enhance.assert_called_once_with("foo.bar", "redirect", None) - self.assertEqual(installer.save.call_count, 1) - installer.restart.assert_called_once_with() + self.config.hsts = False + self.config.redirect = False + self.config.staple = False + self.config.uir = False + self.domain = "example.org" + + def test_no_installer(self): + self.assertRaises( + errors.Error, self.client.enhance_config, [self.domain], None) @mock.patch("certbot.client.enhancements") - def test_enhance_config_no_ask(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, - uir=False, staple=False) - self.assertRaises(errors.Error, self.client.enhance_config, - ["foo.bar"], config, None) + def test_unsupported(self, mock_enhancements): + self.client.installer = mock.MagicMock() + self.client.installer.supported_enhancements.return_value = [] - mock_enhancements.ask.return_value = True - installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = [ - "redirect", "ensure-http-header", "staple-ocsp"] - - config = ConfigHelper(redirect=True, hsts=False, - uir=False, staple=False) - self.client.enhance_config(["foo.bar"], config, None) - installer.enhance.assert_called_with("foo.bar", "redirect", None) - - config = ConfigHelper(redirect=False, hsts=True, - uir=False, staple=False) - self.client.enhance_config(["foo.bar"], config, None) - installer.enhance.assert_called_with("foo.bar", "ensure-http-header", - "Strict-Transport-Security") - - config = ConfigHelper(redirect=False, hsts=False, - uir=True, staple=False) - self.client.enhance_config(["foo.bar"], config, None) - installer.enhance.assert_called_with("foo.bar", "ensure-http-header", - "Upgrade-Insecure-Requests") - - config = ConfigHelper(redirect=False, hsts=False, - uir=False, staple=True) - self.client.enhance_config(["foo.bar"], config, None) - installer.enhance.assert_called_with("foo.bar", "staple-ocsp", None) - - self.assertEqual(installer.save.call_count, 4) - self.assertEqual(installer.restart.call_count, 4) - - @mock.patch("certbot.client.enhancements") - def test_enhance_config_unsupported(self, mock_enhancements): - installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = [] - - config = ConfigHelper(redirect=None, hsts=True, uir=True) - self.client.enhance_config(["foo.bar"], config, None) - installer.enhance.assert_not_called() + self.config.redirect = None + self.config.hsts = True + with mock.patch("certbot.client.logger") as mock_logger: + self.client.enhance_config([self.domain], None) + self.assertEqual(mock_logger.warning.call_count, 1) + self.client.installer.enhance.assert_not_called() mock_enhancements.ask.assert_not_called() - def test_enhance_config_no_installer(self): - config = ConfigHelper(redirect=True, hsts=False, uir=False) - self.assertRaises(errors.Error, self.client.enhance_config, - ["foo.bar"], config, None) + def test_no_ask_hsts(self): + self.config.hsts = True + self._test_with_all_supported() + self.client.installer.enhance.assert_called_with( + self.domain, "ensure-http-header", "Strict-Transport-Security") - @mock.patch("certbot.client.zope.component.getUtility") - @mock.patch("certbot.client.enhancements") - def test_enhance_config_enhance_failure(self, mock_enhancements, - mock_get_utility): - mock_enhancements.ask.return_value = True + def test_no_ask_redirect(self): + self.config.redirect = True + self._test_with_all_supported() + self.client.installer.enhance.assert_called_with( + self.domain, "redirect", None) + + def test_no_ask_staple(self): + self.config.staple = True + self._test_with_all_supported() + self.client.installer.enhance.assert_called_with( + self.domain, "staple-ocsp", None) + + def test_no_ask_uir(self): + self.config.uir = True + self._test_with_all_supported() + self.client.installer.enhance.assert_called_with( + self.domain, "ensure-http-header", "Upgrade-Insecure-Requests") + + def test_enhance_failure(self): + self.client.installer = mock.MagicMock() + self.client.installer.enhance.side_effect = errors.PluginError + self._test_error() + self.client.installer.recovery_routine.assert_called_once_with() + + def test_save_failure(self): + self.client.installer = mock.MagicMock() + self.client.installer.save.side_effect = errors.PluginError + self._test_error() + self.client.installer.recovery_routine.assert_called_once_with() + self.client.installer.save.assert_called_once_with(mock.ANY) + + def test_restart_failure(self): + self.client.installer = mock.MagicMock() + self.client.installer.restart.side_effect = [errors.PluginError, None] + self._test_error_with_rollback() + + def test_restart_failure2(self): installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = ["redirect"] - installer.enhance.side_effect = errors.PluginError - - config = ConfigHelper(redirect=True, hsts=False, uir=False) - - self.assertRaises(errors.PluginError, self.client.enhance_config, - ["foo.bar"], config, None) - installer.recovery_routine.assert_called_once_with() - self.assertEqual(mock_get_utility().add_message.call_count, 1) - - @mock.patch("certbot.client.zope.component.getUtility") - @mock.patch("certbot.client.enhancements") - def test_enhance_config_save_failure(self, mock_enhancements, - mock_get_utility): - mock_enhancements.ask.return_value = True - installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = ["redirect"] - installer.save.side_effect = errors.PluginError - - config = ConfigHelper(redirect=True, hsts=False, uir=False) - - self.assertRaises(errors.PluginError, self.client.enhance_config, - ["foo.bar"], config, None) - installer.recovery_routine.assert_called_once_with() - self.assertEqual(mock_get_utility().add_message.call_count, 1) - - @mock.patch("certbot.client.zope.component.getUtility") - @mock.patch("certbot.client.enhancements") - def test_enhance_config_restart_failure(self, mock_enhancements, - mock_get_utility): - mock_enhancements.ask.return_value = True - installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = ["redirect"] - installer.restart.side_effect = [errors.PluginError, None] - - config = ConfigHelper(redirect=True, hsts=False, uir=False) - - self.assertRaises(errors.PluginError, self.client.enhance_config, - ["foo.bar"], config, None) - - self.assertEqual(mock_get_utility().add_message.call_count, 1) - installer.rollback_checkpoints.assert_called_once_with() - self.assertEqual(installer.restart.call_count, 2) - - @mock.patch("certbot.client.zope.component.getUtility") - @mock.patch("certbot.client.enhancements") - def test_enhance_config_restart_failure2(self, mock_enhancements, - mock_get_utility): - mock_enhancements.ask.return_value = True - installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = ["redirect"] installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError + self.client.installer = installer + self._test_error_with_rollback() - config = ConfigHelper(redirect=True, hsts=False, uir=False) + @mock.patch("certbot.client.enhancements.ask") + def test_ask(self, mock_ask): + self.config.redirect = None + mock_ask.return_value = True + self._test_with_all_supported() - self.assertRaises(errors.PluginError, self.client.enhance_config, - ["foo.bar"], config, None) - self.assertEqual(mock_get_utility().add_message.call_count, 1) - installer.rollback_checkpoints.assert_called_once_with() - self.assertEqual(installer.restart.call_count, 1) + def _test_error_with_rollback(self): + self._test_error() + self.assertTrue(self.client.installer.restart.called) + + def _test_error(self): + self.config.redirect = True + with mock.patch("certbot.client.zope.component.getUtility") as mock_gu: + self.assertRaises( + errors.PluginError, self._test_with_all_supported) + self.assertEqual(mock_gu().add_message.call_count, 1) + + def _test_with_all_supported(self): + if self.client.installer is None: + self.client.installer = mock.MagicMock() + self.client.installer.supported_enhancements.return_value = [ + "ensure-http-header", "redirect", "staple-ocsp"] + self.client.enhance_config([self.domain], None) + self.assertEqual(self.client.installer.save.call_count, 1) + self.assertEqual(self.client.installer.restart.call_count, 1) class RollbackTest(unittest.TestCase): From 93f0846fa49afc0af745472ea7b3881986696c65 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 5 Dec 2016 09:09:43 -0800 Subject: [PATCH 278/331] Testfarm test new leauto (#3845) Test farm tests should test the version of letsencrypt-auto that's in the git tree, not the one from the previous release. * Test the new leauto, not the previously released one --- .../scripts/test_letsencrypt_auto_certonly_standalone.sh | 2 +- tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh b/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh index f4aef11fe..556f95e8c 100755 --- a/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh +++ b/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh @@ -7,7 +7,7 @@ #public_ip=$(curl -s http://169.254.169.254/2014-11-05/meta-data/public-ipv4) #private_ip=$(curl -s http://169.254.169.254/2014-11-05/meta-data/local-ipv4) -cd letsencrypt +cd letsencrypt/letsencrypt-auto-source ./letsencrypt-auto --os-packages-only --debug --version ./letsencrypt-auto certonly --no-self-upgrade -v --standalone --debug \ --text --agree-dev-preview --agree-tos \ diff --git a/tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh b/tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh index 234e70f68..c55e12e8b 100755 --- a/tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh +++ b/tests/letstest/scripts/test_letsencrypt_auto_venv_only.sh @@ -4,4 +4,4 @@ cd letsencrypt # help installs virtualenv and does nothing else -./letsencrypt-auto -v --debug --help all +./letsencrypt-auto-source/letsencrypt-auto -v --debug --help all From 65d9e997e54ee215079e396e8d441a43f10a68ab Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Mon, 5 Dec 2016 15:22:14 -0800 Subject: [PATCH 279/331] Refactor cli_test.py and main_test.py (#3828) * Begin breaking out cli_test.py * simplify main * refactor porse tests * move determine account tests to main_test.py * move duplicate cert test to main_test.py * move cli stuff out of the way * add test_renewal.py * move error test into error_handler_test.py * move test_read_file * move test_no_gui out of MainTest * move test_install_abspath to parsetest * Move main tests into main_test.py * move cli tests back into cli_test.py * clean up cli_test.py * move punycode test to util_test.py * Fix NameError from missing plugins_disco * Fix linting errors * test_renewal.py -> renewal_test.py * rm not_cli_test.py * Move main._handle_exception test to main_test.py * Move renewal import in renewal_test.py from @ohemorange comments * certbot.tests.test_util -> certbot.tests.util * Fix issues from rebasing. * Fix testing issue with option_was_set * fix linting issue --- certbot-apache/certbot_apache/tests/util.py | 2 +- certbot-nginx/certbot_nginx/tests/util.py | 2 +- certbot/main.py | 44 +- certbot/plugins/common_test.py | 2 +- certbot/plugins/manual_test.py | 2 +- certbot/plugins/script_test.py | 2 +- certbot/plugins/standalone_test.py | 2 +- certbot/plugins/util_test.py | 2 +- certbot/plugins/webroot_test.py | 2 +- certbot/tests/account_test.py | 4 +- certbot/tests/acme_util.py | 4 +- certbot/tests/cli_test.py | 1135 ++----------------- certbot/tests/client_test.py | 2 +- certbot/tests/crypto_util_test.py | 2 +- certbot/tests/display/ops_test.py | 2 +- certbot/tests/error_handler_test.py | 1 + certbot/tests/main_test.py | 928 ++++++++++++++- certbot/tests/renewal_test.py | 30 + certbot/tests/storage_test.py | 12 +- certbot/tests/{test_util.py => util.py} | 47 + certbot/tests/util_test.py | 8 +- 21 files changed, 1139 insertions(+), 1096 deletions(-) create mode 100644 certbot/tests/renewal_test.py rename certbot/tests/{test_util.py => util.py} (68%) diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 050876687..6a0a83615 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -13,7 +13,7 @@ from certbot.display import util as display_util from certbot.plugins import common -from certbot.tests import test_util +from certbot.tests import util as test_util from certbot_apache import configurator from certbot_apache import constants diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 96fdac527..2fb866b77 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -11,7 +11,7 @@ from acme import jose from certbot import configuration -from certbot.tests import test_util +from certbot.tests import util as test_util from certbot.plugins import common diff --git a/certbot/main.py b/certbot/main.py index 37af009e3..471dcd838 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -735,6 +735,29 @@ def make_or_verify_core_dir(directory, mode, uid, strict): except OSError as error: raise errors.Error(_PERM_ERR_FMT.format(error)) +def make_or_verify_needed_dirs(config): + """Create or verify existance of config, work, or logs directories""" + make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, + os.geteuid(), config.strict_permissions) + make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, + os.geteuid(), config.strict_permissions) + # TODO: logs might contain sensitive data such as contents of the + # private key! #525 + make_or_verify_core_dir(config.logs_dir, 0o700, + os.geteuid(), config.strict_permissions) + + +def set_displayer(config): + """Set the displayer""" + if config.quiet: + config.noninteractive_mode = True + displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w")) + elif config.noninteractive_mode: + displayer = display_util.NoninteractiveDisplay(sys.stdout) + else: + displayer = display_util.FileDisplay(sys.stdout) + zope.component.provideUtility(displayer) + def main(cli_args=sys.argv[1:]): """Command line argument parsing and main script execution.""" @@ -746,17 +769,12 @@ def main(cli_args=sys.argv[1:]): config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) - make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, - os.geteuid(), config.strict_permissions) - make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, - os.geteuid(), config.strict_permissions) - # TODO: logs might contain sensitive data such as contents of the - # private key! #525 - make_or_verify_core_dir(config.logs_dir, 0o700, - os.geteuid(), config.strict_permissions) + make_or_verify_needed_dirs(config) + # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery setup_logging(config) + cli.possible_deprecation_warning(config) logger.debug("certbot version: %s", certbot.__version__) @@ -766,15 +784,7 @@ def main(cli_args=sys.argv[1:]): sys.excepthook = functools.partial(_handle_exception, config=config) - # Displayer - if config.quiet: - config.noninteractive_mode = True - displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w")) - elif config.noninteractive_mode: - displayer = display_util.NoninteractiveDisplay(sys.stdout) - else: - displayer = display_util.FileDisplay(sys.stdout) - zope.component.provideUtility(displayer) + set_displayer(config) # Reporter report = reporter.Reporter(config) diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py index f3ea714c4..eee768e18 100644 --- a/certbot/plugins/common_test.py +++ b/certbot/plugins/common_test.py @@ -10,7 +10,7 @@ from acme import jose from certbot import achallenges from certbot.tests import acme_util -from certbot.tests import test_util +from certbot.tests import util as test_util class NamespaceFunctionsTest(unittest.TestCase): diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index 828281951..154b0d729 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -11,7 +11,7 @@ from certbot import achallenges from certbot import errors from certbot.tests import acme_util -from certbot.tests import test_util +from certbot.tests import util as test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) diff --git a/certbot/plugins/script_test.py b/certbot/plugins/script_test.py index 0c13d84db..1fe57a8dc 100644 --- a/certbot/plugins/script_test.py +++ b/certbot/plugins/script_test.py @@ -12,7 +12,7 @@ from certbot import achallenges from certbot import errors from certbot.tests import acme_util -from certbot.tests import test_util +from certbot.tests import util as test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index cb82ae7d8..249d409b5 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -15,7 +15,7 @@ from certbot import errors from certbot import interfaces from certbot.tests import acme_util -from certbot.tests import test_util +from certbot.tests import util as test_util class ServerManagerTest(unittest.TestCase): diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index f8ffede86..7ec8d6aa8 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -6,7 +6,7 @@ import unittest import mock from certbot.plugins.util import PSUTIL_REQUIREMENT -from certbot.tests import test_util +from certbot.tests import util as test_util class PathSurgeryTest(unittest.TestCase): diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py index 2aa7e8acc..aca5544c3 100644 --- a/certbot/plugins/webroot_test.py +++ b/certbot/plugins/webroot_test.py @@ -21,7 +21,7 @@ from certbot import errors from certbot.display import util as display_util from certbot.tests import acme_util -from certbot.tests import test_util +from certbot.tests import util as test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index 41b835838..1c50025d7 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -14,10 +14,10 @@ from acme import messages from certbot import errors -from certbot.tests import test_util +from certbot.tests import util -KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem")) +KEY = jose.JWKRSA.load(util.load_vector("rsa512_key_2.pem")) class AccountTest(unittest.TestCase): diff --git a/certbot/tests/acme_util.py b/certbot/tests/acme_util.py index de64dfef9..3168349c9 100644 --- a/certbot/tests/acme_util.py +++ b/certbot/tests/acme_util.py @@ -7,10 +7,10 @@ from acme import challenges from acme import jose from acme import messages -from certbot.tests import test_util +from certbot.tests import util -KEY = test_util.load_rsa_private_key('rsa512_key.pem') +KEY = util.load_rsa_private_key('rsa512_key.pem') # Challenges HTTP01 = challenges.HTTP01( diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 54ae74f95..0c9f73f6a 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -1,99 +1,78 @@ """Tests for certbot.cli.""" -# Many tests in this file should be moved into -# main_test.py and renewal_test.py. See #2716. -# pylint: disable=too-many-lines -from __future__ import print_function - import argparse import functools -import itertools -import os -import shutil -import traceback -import tempfile import unittest +import os +import tempfile -import mock import six +import mock from six.moves import reload_module # pylint: disable=import-error -from acme import jose - -from certbot import account from certbot import cli -from certbot import configuration from certbot import constants -from certbot import crypto_util from certbot import errors -from certbot import util -from certbot import main -from certbot import renewal -from certbot import storage - from certbot.plugins import disco -from certbot.plugins import manual -from certbot.tests import storage_test -from certbot.tests import test_util +def reset_set_by_cli(): + '''Reset the state of the `set_by_cli` function''' + cli.set_by_cli.detector = None + +class TestReadFile(unittest.TestCase): + '''Test cli.read_file''' + def test_read_file(self): + tmp_dir = tempfile.mkdtemp() + rel_test_path = os.path.relpath(os.path.join(tmp_dir, 'foo')) + self.assertRaises( + argparse.ArgumentTypeError, cli.read_file, rel_test_path) + + test_contents = b'bar\n' + with open(rel_test_path, 'wb') as f: + f.write(test_contents) + + path, contents = cli.read_file(rel_test_path) + self.assertEqual(path, os.path.abspath(path)) + self.assertEqual(contents, test_contents) -CERT = test_util.vector_path('cert.pem') -CSR = test_util.vector_path('csr.der') -KEY = test_util.vector_path('rsa256_key.pem') +class ParseTest(unittest.TestCase): + '''Test the cli args entrypoint''' - -class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods - """Tests for different commands.""" + @classmethod + def setUpClass(cls): + cls.plugins = disco.PluginsRegistry.find_all() + cls.parse = functools.partial(cli.prepare_and_parse_args, cls.plugins) def setUp(self): - self.tmp_dir = tempfile.mkdtemp() - self.config_dir = os.path.join(self.tmp_dir, 'config') - self.work_dir = os.path.join(self.tmp_dir, 'work') - self.logs_dir = os.path.join(self.tmp_dir, 'logs') - os.mkdir(self.logs_dir) - self.standard_args = ['--config-dir', self.config_dir, - '--work-dir', self.work_dir, - '--logs-dir', self.logs_dir, '--text'] - - def tearDown(self): - shutil.rmtree(self.tmp_dir) - # Reset globals in cli - # pylint: disable=protected-access - cli._parser = cli.set_by_cli.detector = None - - def _call(self, args, stdout=None): - "Run the cli with output streams and actual client mocked out" - with mock.patch('certbot.main.client') as client: - ret, stdout, stderr = self._call_no_clientmock(args, stdout) - return ret, stdout, stderr, client - - def _call_no_clientmock(self, args, stdout=None): - "Run the client with output streams mocked out" - args = self.standard_args + args - - toy_stdout = stdout if stdout else six.StringIO() - with mock.patch('certbot.main.sys.stdout', new=toy_stdout): - with mock.patch('certbot.main.sys.stderr') as stderr: - ret = main.main(args[:]) # NOTE: parser can alter its args! - return ret, toy_stdout, stderr - - def test_no_flags(self): - with mock.patch('certbot.main.run') as mock_run: - self._call([]) - self.assertEqual(1, mock_run.call_count) + reset_set_by_cli() def _help_output(self, args): "Run a command, and return the ouput string for scrutiny" output = six.StringIO() - self.assertRaises(SystemExit, self._call, args, output) - out = output.getvalue() - return out + with mock.patch('certbot.main.sys.stdout', new=output): + with mock.patch('certbot.main.sys.stderr'): + self.assertRaises(SystemExit, self.parse, args, output) + return output.getvalue() + + def test_install_abspath(self): + cert = 'cert' + key = 'key' + chain = 'chain' + fullchain = 'fullchain' + + with mock.patch('certbot.main.install'): + namespace = self.parse(['install', '--cert-path', cert, + '--key-path', 'key', '--chain-path', + 'chain', '--fullchain-path', 'fullchain']) + + self.assertEqual(namespace.cert_path, os.path.abspath(cert)) + self.assertEqual(namespace.key_path, os.path.abspath(key)) + self.assertEqual(namespace.chain_path, os.path.abspath(chain)) + self.assertEqual(namespace.fullchain_path, os.path.abspath(fullchain)) def test_help(self): - self.assertRaises(SystemExit, self._call, ['--help']) - self.assertRaises(SystemExit, self._call, ['--help', 'all']) - plugins = disco.PluginsRegistry.find_all() + self._help_output(['--help']) # assert SystemExit is raised here out = self._help_output(['--help', 'all']) self.assertTrue("--configurator" in out) self.assertTrue("how a cert is deployed" in out) @@ -102,15 +81,15 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue("--dialog" not in out) out = self._help_output(['-h', 'nginx']) - if "nginx" in plugins: + if "nginx" in self.plugins: # may be false while building distributions without plugins self.assertTrue("--nginx-ctl" in out) self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--checkpoints" not in out) out = self._help_output(['-h']) - self.assertTrue("letsencrypt-auto" not in out) # test cli.cli_command - if "nginx" in plugins: + self.assertTrue("letsencrypt-auto" not in out) # test cli.cli_command + if "nginx" in self.plugins: self.assertTrue("Use the Nginx plugin" in out) else: self.assertTrue("(nginx support is experimental" in out) @@ -141,339 +120,67 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue("--key-path" not in out) out = self._help_output(['-h']) - self.assertTrue(cli.usage_strings(plugins)[0] in out) - - def test_version_string_program_name(self): - toy_out = six.StringIO() - toy_err = six.StringIO() - with mock.patch('certbot.main.sys.stdout', new=toy_out): - with mock.patch('certbot.main.sys.stderr', new=toy_err): - try: - main.main(["--version"]) - except SystemExit: - pass - finally: - output = toy_out.getvalue() or toy_err.getvalue() - self.assertTrue("certbot" in output, "Output is {0}".format(output)) - toy_out.close() - toy_err.close() - - def _cli_missing_flag(self, args, message): - "Ensure that a particular error raises a missing cli flag error containing message" - exc = None - try: - with mock.patch('certbot.main.sys.stderr'): - main.main(self.standard_args + args[:]) # NOTE: parser can alter its args! - except errors.MissingCommandlineFlag as exc_: - exc = exc_ - self.assertTrue(message in str(exc)) - self.assertTrue(exc is not None) - - def test_noninteractive(self): - args = ['-n', 'certonly'] - self._cli_missing_flag(args, "specify a plugin") - args.extend(['--standalone', '-d', 'eg.is']) - self._cli_missing_flag(args, "register before running") - with mock.patch('certbot.main._auth_from_domains'): - with mock.patch('certbot.main.client.acme_from_config_key'): - args.extend(['--email', 'io@io.is']) - self._cli_missing_flag(args, "--agree-tos") - - @mock.patch('certbot.main.renew') - def test_no_gui(self, renew): - args = ['renew', '--dialog'] - # --dialog should have no effect - self._call(args) - self.assertTrue(renew.call_args[0][0].noninteractive_mode) - - @mock.patch('certbot.main.client.acme_client.Client') - @mock.patch('certbot.main._determine_account') - @mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate') - @mock.patch('certbot.main._auth_from_domains') - def test_user_agent(self, afd, _obt, det, _client): - # Normally the client is totally mocked out, but here we need more - # arguments to automate it... - args = ["--standalone", "certonly", "-m", "none@none.com", - "-d", "example.com", '--agree-tos'] + self.standard_args - det.return_value = mock.MagicMock(), None - afd.return_value = "newcert", mock.MagicMock() - - with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: - self._call_no_clientmock(args) - os_ver = util.get_os_info_ua() - ua = acme_net.call_args[1]["user_agent"] - self.assertTrue(os_ver in ua) - import platform - plat = platform.platform() - if "linux" in plat.lower(): - self.assertTrue(util.get_os_info_ua() in ua) - - with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: - ua = "bandersnatch" - args += ["--user-agent", ua] - self._call_no_clientmock(args) - acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) - - def test_install_abspath(self): - cert = 'cert' - key = 'key' - chain = 'chain' - fullchain = 'fullchain' - - with mock.patch('certbot.main.install') as mock_install: - self._call(['install', '--cert-path', cert, '--key-path', 'key', - '--chain-path', 'chain', - '--fullchain-path', 'fullchain']) - - args = mock_install.call_args[0][0] - self.assertEqual(args.cert_path, os.path.abspath(cert)) - self.assertEqual(args.key_path, os.path.abspath(key)) - self.assertEqual(args.chain_path, os.path.abspath(chain)) - self.assertEqual(args.fullchain_path, os.path.abspath(fullchain)) - - @mock.patch('certbot.main.plug_sel.record_chosen_plugins') - @mock.patch('certbot.main.plug_sel.pick_installer') - def test_installer_selection(self, mock_pick_installer, _rec): - self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', - '--key-path', 'key', '--chain-path', 'chain']) - self.assertEqual(mock_pick_installer.call_count, 1) - - @mock.patch('certbot.util.exe_exists') - def test_configurator_selection(self, mock_exe_exists): - mock_exe_exists.return_value = True - real_plugins = disco.PluginsRegistry.find_all() - args = ['--apache', '--authenticator', 'standalone'] - - # This needed two calls to find_all(), which we're avoiding for now - # because of possible side effects: - # https://github.com/letsencrypt/letsencrypt/commit/51ed2b681f87b1eb29088dd48718a54f401e4855 - #with mock.patch('certbot.cli.plugins_testable') as plugins: - # plugins.return_value = {"apache": True, "nginx": True} - # ret, _, _, _ = self._call(args) - # self.assertTrue("Too many flags setting" in ret) - - args = ["install", "--nginx", "--cert-path", "/tmp/blah", "--key-path", "/tmp/blah", - "--nginx-server-root", "/nonexistent/thing", "-d", - "example.com", "--debug"] - if "nginx" in real_plugins: - # Sending nginx a non-existent conf dir will simulate misconfiguration - # (we can only do that if certbot-nginx is actually present) - ret, _, _, _ = self._call(args) - self.assertTrue("The nginx plugin is not working" in ret) - self.assertTrue("MisconfigurationError" in ret) - - self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") - - with mock.patch("certbot.main._init_le_client") as mock_init: - with mock.patch("certbot.main._auth_from_domains") as mock_afd: - mock_afd.return_value = (mock.MagicMock(), mock.MagicMock()) - self._call(["certonly", "--manual", "-d", "foo.bar"]) - unused_config, auth, unused_installer = mock_init.call_args[0] - self.assertTrue(isinstance(auth, manual.Authenticator)) - - with mock.patch('certbot.main.obtain_cert') as mock_certonly: - self._call(["auth", "--standalone"]) - self.assertEqual(1, mock_certonly.call_count) - - def test_rollback(self): - _, _, _, client = self._call(['rollback']) - self.assertEqual(1, client.rollback.call_count) - - _, _, _, client = self._call(['rollback', '--checkpoints', '123']) - client.rollback.assert_called_once_with( - mock.ANY, 123, mock.ANY, mock.ANY) - - def test_config_changes(self): - _, _, _, client = self._call(['config_changes']) - self.assertEqual(1, client.view_config_changes.call_count) - - @mock.patch('certbot.cert_manager.update_live_symlinks') - def test_update_symlinks(self, mock_cert_manager): - self._call_no_clientmock(['update_symlinks']) - self.assertEqual(1, mock_cert_manager.call_count) - - @mock.patch('certbot.cert_manager.certificates') - def test_certificates(self, mock_cert_manager): - self._call_no_clientmock(['certificates']) - self.assertEqual(1, mock_cert_manager.call_count) - - def test_plugins(self): - flags = ['--init', '--prepare', '--authenticators', '--installers'] - for args in itertools.chain( - *(itertools.combinations(flags, r) - for r in six.moves.range(len(flags)))): - self._call(['plugins'] + list(args)) - - @mock.patch('certbot.main.plugins_disco') - @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics') - def test_plugins_no_args(self, _det, mock_disco): - ifaces = [] - plugins = mock_disco.PluginsRegistry.find_all() - - _, stdout, _, _ = self._call(['plugins']) - plugins.visible.assert_called_once_with() - plugins.visible().ifaces.assert_called_once_with(ifaces) - filtered = plugins.visible().ifaces() - self.assertEqual(stdout.getvalue().strip(), str(filtered)) - - @mock.patch('certbot.main.plugins_disco') - @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics') - def test_plugins_init(self, _det, mock_disco): - ifaces = [] - plugins = mock_disco.PluginsRegistry.find_all() - - _, stdout, _, _ = self._call(['plugins', '--init']) - plugins.visible.assert_called_once_with() - plugins.visible().ifaces.assert_called_once_with(ifaces) - filtered = plugins.visible().ifaces() - self.assertEqual(filtered.init.call_count, 1) - filtered.verify.assert_called_once_with(ifaces) - verified = filtered.verify() - self.assertEqual(stdout.getvalue().strip(), str(verified)) - - @mock.patch('certbot.main.plugins_disco') - @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics') - def test_plugins_prepare(self, _det, mock_disco): - ifaces = [] - plugins = mock_disco.PluginsRegistry.find_all() - _, stdout, _, _ = self._call(['plugins', '--init', '--prepare']) - plugins.visible.assert_called_once_with() - plugins.visible().ifaces.assert_called_once_with(ifaces) - filtered = plugins.visible().ifaces() - self.assertEqual(filtered.init.call_count, 1) - filtered.verify.assert_called_once_with(ifaces) - verified = filtered.verify() - verified.prepare.assert_called_once_with() - verified.available.assert_called_once_with() - available = verified.available() - self.assertEqual(stdout.getvalue().strip(), str(available)) - - def test_certonly_abspath(self): - cert = 'cert' - key = 'key' - chain = 'chain' - fullchain = 'fullchain' - - with mock.patch('certbot.main.obtain_cert') as mock_obtaincert: - self._call(['certonly', '--cert-path', cert, '--key-path', 'key', - '--chain-path', 'chain', - '--fullchain-path', 'fullchain']) - - config, unused_plugins = mock_obtaincert.call_args[0] - self.assertEqual(config.cert_path, os.path.abspath(cert)) - self.assertEqual(config.key_path, os.path.abspath(key)) - self.assertEqual(config.chain_path, os.path.abspath(chain)) - self.assertEqual(config.fullchain_path, os.path.abspath(fullchain)) - - def test_certonly_bad_args(self): - try: - self._call(['-a', 'bad_auth', 'certonly']) - assert False, "Exception should have been raised" - except errors.PluginSelectionError as e: - self.assertTrue('The requested bad_auth plugin does not appear' in str(e)) - - def test_punycode_ok(self): - # Punycode is now legal, so no longer an error; instead check - # that it's _not_ an error (at the initial sanity check stage) - util.enforce_domain_sanity('this.is.xn--ls8h.tld') - - def test_check_config_sanity_domain(self): - # FQDN - self.assertRaises(errors.ConfigurationError, - self._call, - ['-d', 'a' * 64]) - # FQDN 2 - self.assertRaises(errors.ConfigurationError, - self._call, - ['-d', (('a' * 50) + '.') * 10]) - # Wildcard - self.assertRaises(errors.ConfigurationError, - self._call, - ['-d', '*.wildcard.tld']) - - # Bare IP address (this is actually a different error message now) - self.assertRaises(errors.ConfigurationError, - self._call, - ['-d', '204.11.231.35']) - - def test_csr_with_besteffort(self): - self.assertRaises( - errors.Error, self._call, - 'certonly --csr {0} --allow-subset-of-names'.format(CSR).split()) - - def test_run_with_csr(self): - # This is an error because you can only use --csr with certonly - try: - self._call(['--csr', CSR]) - except errors.Error as e: - assert "Please try the certonly" in repr(e) - return - assert False, "Expected supplying --csr to fail with default verb" - - def test_csr_with_no_domains(self): - self.assertRaises( - errors.Error, self._call, - 'certonly --csr {0}'.format( - test_util.vector_path('csr-nonames.pem')).split()) - - def test_csr_with_inconsistent_domains(self): - self.assertRaises( - errors.Error, self._call, - 'certonly -d example.org --csr {0}'.format(CSR).split()) - - def _get_argument_parser(self): - plugins = disco.PluginsRegistry.find_all() - return functools.partial(cli.prepare_and_parse_args, plugins) + self.assertTrue(cli.usage_strings(self.plugins)[0] in out) def test_parse_domains(self): - parse = self._get_argument_parser() - short_args = ['-d', 'example.com'] - namespace = parse(short_args) + namespace = self.parse(short_args) self.assertEqual(namespace.domains, ['example.com']) short_args = ['-d', 'trailing.period.com.'] - namespace = parse(short_args) + namespace = self.parse(short_args) self.assertEqual(namespace.domains, ['trailing.period.com']) short_args = ['-d', 'example.com,another.net,third.org,example.com'] - namespace = parse(short_args) + namespace = self.parse(short_args) self.assertEqual(namespace.domains, ['example.com', 'another.net', 'third.org']) long_args = ['--domains', 'example.com'] - namespace = parse(long_args) + namespace = self.parse(long_args) self.assertEqual(namespace.domains, ['example.com']) long_args = ['--domains', 'trailing.period.com.'] - namespace = parse(long_args) + namespace = self.parse(long_args) self.assertEqual(namespace.domains, ['trailing.period.com']) long_args = ['--domains', 'example.com,another.net,example.com'] - namespace = parse(long_args) + namespace = self.parse(long_args) self.assertEqual(namespace.domains, ['example.com', 'another.net']) def test_preferred_challenges(self): from acme.challenges import HTTP01, TLSSNI01, DNS01 - parse = self._get_argument_parser() short_args = ['--preferred-challenges', 'http, tls-sni-01, dns'] - namespace = parse(short_args) + namespace = self.parse(short_args) self.assertEqual(namespace.pref_challs, [HTTP01, TLSSNI01, DNS01]) short_args = ['--preferred-challenges', 'jumping-over-the-moon'] - self.assertRaises(argparse.ArgumentTypeError, parse, short_args) + self.assertRaises(argparse.ArgumentTypeError, self.parse, short_args) def test_server_flag(self): - parse = self._get_argument_parser() - namespace = parse('--server example.com'.split()) + namespace = self.parse('--server example.com'.split()) self.assertEqual(namespace.server, 'example.com') + def test_must_staple_flag(self): + short_args = ['--must-staple'] + namespace = self.parse(short_args) + self.assertTrue(namespace.must_staple) + self.assertTrue(namespace.staple) + + def test_no_gui(self): + args = ['renew', '--dialog'] + stderr = six.StringIO() + with mock.patch('certbot.main.sys.stderr', new=stderr): + namespace = self.parse(args) + + self.assertTrue(namespace.noninteractive_mode) + self.assertTrue("--dialog is deprecated" in stderr.getvalue()) + def _check_server_conflict_message(self, parser_args, conflicting_args): - parse = self._get_argument_parser() try: - parse(parser_args) + self.parse(parser_args) self.fail( # pragma: no cover "The following flags didn't conflict with " '--server: {0}'.format(', '.join(conflicting_args))) @@ -482,36 +189,15 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods for arg in conflicting_args: self.assertTrue(arg in str(error)) - def test_must_staple_flag(self): - parse = self._get_argument_parser() - short_args = ['--must-staple'] - namespace = parse(short_args) - self.assertTrue(namespace.must_staple) - self.assertTrue(namespace.staple) - def test_staging_flag(self): - parse = self._get_argument_parser() short_args = ['--staging'] - namespace = parse(short_args) + namespace = self.parse(short_args) self.assertTrue(namespace.staging) self.assertEqual(namespace.server, constants.STAGING_URI) short_args += '--server example.com'.split() self._check_server_conflict_message(short_args, '--staging') - def test_option_was_set(self): - key_size_option = 'rsa_key_size' - key_size_value = cli.flag_default(key_size_option) - self._get_argument_parser()( - '--rsa-key-size {0}'.format(key_size_value).split()) - - self.assertTrue(cli.option_was_set(key_size_option, key_size_value)) - self.assertTrue(cli.option_was_set('no_verify_ssl', True)) - - config_dir_option = 'config_dir' - self.assertFalse(cli.option_was_set( - config_dir_option, cli.flag_default(config_dir_option))) - def _assert_dry_run_flag_worked(self, namespace, existing_account): self.assertTrue(namespace.dry_run) self.assertTrue(namespace.break_my_certs) @@ -526,26 +212,25 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertFalse(namespace.register_unsafely_without_email) def test_dry_run_flag(self): - parse = self._get_argument_parser() config_dir = tempfile.mkdtemp() short_args = '--dry-run --config-dir {0}'.format(config_dir).split() - self.assertRaises(errors.Error, parse, short_args) + self.assertRaises(errors.Error, self.parse, short_args) self._assert_dry_run_flag_worked( - parse(short_args + ['auth']), False) + self.parse(short_args + ['auth']), False) self._assert_dry_run_flag_worked( - parse(short_args + ['certonly']), False) + self.parse(short_args + ['certonly']), False) self._assert_dry_run_flag_worked( - parse(short_args + ['renew']), False) + self.parse(short_args + ['renew']), False) account_dir = os.path.join(config_dir, constants.ACCOUNTS_DIR) os.mkdir(account_dir) os.mkdir(os.path.join(account_dir, 'fake_account_dir')) - self._assert_dry_run_flag_worked(parse(short_args + ['auth']), True) - self._assert_dry_run_flag_worked(parse(short_args + ['renew']), True) + self._assert_dry_run_flag_worked(self.parse(short_args + ['auth']), True) + self._assert_dry_run_flag_worked(self.parse(short_args + ['renew']), True) short_args += ['certonly'] - self._assert_dry_run_flag_worked(parse(short_args), True) + self._assert_dry_run_flag_worked(self.parse(short_args), True) short_args += '--server example.com'.split() conflicts = ['--dry-run'] @@ -555,645 +240,17 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods conflicts += ['--staging'] self._check_server_conflict_message(short_args, conflicts) - def _certonly_new_request_common(self, mock_client, args=None): - with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: - mock_renewal.return_value = ("newcert", None) - with mock.patch('certbot.main._init_le_client') as mock_init: - mock_init.return_value = mock_client - if args is None: - args = [] - args += '-d foo.bar -a standalone certonly'.split() - self._call(args) + def test_option_was_set(self): + key_size_option = 'rsa_key_size' + key_size_value = cli.flag_default(key_size_option) + self.parse('--rsa-key-size {0}'.format(key_size_value).split()) - @mock.patch('certbot.main.zope.component.getUtility') - def test_certonly_dry_run_new_request_success(self, mock_get_utility): - mock_client = mock.MagicMock() - mock_client.obtain_and_enroll_certificate.return_value = None - self._certonly_new_request_common(mock_client, ['--dry-run']) - self.assertEqual( - mock_client.obtain_and_enroll_certificate.call_count, 1) - self.assertTrue( - 'dry run' in mock_get_utility().add_message.call_args[0][0]) - # Asserts we don't suggest donating after a successful dry run - self.assertEqual(mock_get_utility().add_message.call_count, 1) + self.assertTrue(cli.option_was_set(key_size_option, key_size_value)) + self.assertTrue(cli.option_was_set('no_verify_ssl', True)) - @mock.patch('certbot.crypto_util.notAfter') - @mock.patch('certbot.main.zope.component.getUtility') - def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): - cert_path = '/etc/letsencrypt/live/foo.bar' - date = '1970-01-01' - mock_notAfter().date.return_value = date - - mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path) - mock_client = mock.MagicMock() - mock_client.obtain_and_enroll_certificate.return_value = mock_lineage - self._certonly_new_request_common(mock_client) - self.assertEqual( - mock_client.obtain_and_enroll_certificate.call_count, 1) - cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] - self.assertTrue(cert_path in cert_msg) - self.assertTrue(date in cert_msg) - self.assertTrue( - 'donate' in mock_get_utility().add_message.call_args[0][0]) - - def test_certonly_new_request_failure(self): - mock_client = mock.MagicMock() - mock_client.obtain_and_enroll_certificate.return_value = False - self.assertRaises(errors.Error, - self._certonly_new_request_common, mock_client) - - def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, - args=None, should_renew=True, error_expected=False): - # pylint: disable=too-many-locals,too-many-arguments - cert_path = test_util.vector_path('cert.pem') - chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' - mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path) - mock_lineage.should_autorenew.return_value = due_for_renewal - mock_lineage.has_pending_deployment.return_value = False - mock_certr = mock.MagicMock() - mock_key = mock.MagicMock(pem='pem_key') - mock_client = mock.MagicMock() - stdout = None - mock_client.obtain_certificate.return_value = (mock_certr, 'chain', - mock_key, 'csr') - try: - with mock.patch('certbot.main._find_duplicative_certs') as mock_fdc: - mock_fdc.return_value = (mock_lineage, None) - with mock.patch('certbot.main._init_le_client') as mock_init: - mock_init.return_value = mock_client - get_utility_path = 'certbot.main.zope.component.getUtility' - with mock.patch(get_utility_path) as mock_get_utility: - with mock.patch('certbot.main.renewal.OpenSSL') as mock_ssl: - mock_latest = mock.MagicMock() - mock_latest.get_issuer.return_value = "Fake fake" - mock_ssl.crypto.load_certificate.return_value = mock_latest - with mock.patch('certbot.main.renewal.crypto_util'): - if not args: - args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly'] - if extra_args: - args += extra_args - try: - ret, stdout, _, _ = self._call(args) - if ret: - print("Returned", ret) - raise AssertionError(ret) - assert not error_expected, "renewal should have errored" - except: # pylint: disable=bare-except - if not error_expected: - raise AssertionError( - "Unexpected renewal error:\n" + - traceback.format_exc()) - - if should_renew: - mock_client.obtain_certificate.assert_called_once_with(['isnot.org']) - else: - self.assertEqual(mock_client.obtain_certificate.call_count, 0) - except: - self._dump_log() - raise - finally: - if log_out: - with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: - self.assertTrue(log_out in lf.read()) - - return mock_lineage, mock_get_utility, stdout - - def test_certonly_renewal(self): - lineage, get_utility, _ = self._test_renewal_common(True, []) - self.assertEqual(lineage.save_successor.call_count, 1) - lineage.update_all_links_to.assert_called_once_with( - lineage.latest_common_version()) - cert_msg = get_utility().add_message.call_args_list[0][0][0] - self.assertTrue('fullchain.pem' in cert_msg) - self.assertTrue('donate' in get_utility().add_message.call_args[0][0]) - - def test_certonly_renewal_triggers(self): - # --dry-run should force renewal - _, get_utility, _ = self._test_renewal_common(False, ['--dry-run', '--keep'], - log_out="simulating renewal") - self.assertEqual(get_utility().add_message.call_count, 1) - self.assertTrue('dry run' in get_utility().add_message.call_args[0][0]) - - self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'], - log_out="Auto-renewal forced") - self.assertEqual(get_utility().add_message.call_count, 1) - - self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], - log_out="not yet due", should_renew=False) - - def _dump_log(self): - print("Logs:") - log_path = os.path.join(self.logs_dir, "letsencrypt.log") - if os.path.exists(log_path): - with open(log_path) as lf: - print(lf.read()) - - def _make_lineage(self, testfile): - """Creates a lineage defined by testfile. - - This creates the archive, live, and renewal directories if - necessary and creates a simple lineage. - - :param str testfile: configuration file to base the lineage on - - :returns: path to the renewal conf file for the created lineage - :rtype: str - - """ - lineage_name = testfile[:-len('.conf')] - - conf_dir = os.path.join( - self.config_dir, constants.RENEWAL_CONFIGS_DIR) - archive_dir = os.path.join( - self.config_dir, constants.ARCHIVE_DIR, lineage_name) - live_dir = os.path.join( - self.config_dir, constants.LIVE_DIR, lineage_name) - - for directory in (archive_dir, conf_dir, live_dir,): - if not os.path.exists(directory): - os.makedirs(directory) - - sample_archive = test_util.vector_path('sample-archive') - for kind in os.listdir(sample_archive): - shutil.copyfile(os.path.join(sample_archive, kind), - os.path.join(archive_dir, kind)) - - for kind in storage.ALL_FOUR: - os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)), - os.path.join(live_dir, '{0}.pem'.format(kind))) - - conf_path = os.path.join(self.config_dir, conf_dir, testfile) - with open(test_util.vector_path(testfile)) as src: - with open(conf_path, 'w') as dst: - dst.writelines( - line.replace('MAGICDIR', self.config_dir) for line in src) - - return conf_path - - def test_renew_verb(self): - self._make_lineage('sample-renewal.conf') - args = ["renew", "--dry-run", "-tvv"] - self._test_renewal_common(True, [], args=args, should_renew=True) - - def test_quiet_renew(self): - self._make_lineage('sample-renewal.conf') - args = ["renew", "--dry-run"] - _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) - out = stdout.getvalue() - self.assertTrue("renew" in out) - - args = ["renew", "--dry-run", "-q"] - _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) - out = stdout.getvalue() - self.assertEqual("", out) - - def test_renew_hook_validation(self): - self._make_lineage('sample-renewal.conf') - args = ["renew", "--dry-run", "--post-hook=no-such-command"] - self._test_renewal_common(True, [], args=args, should_renew=False, - error_expected=True) - - def test_renew_no_hook_validation(self): - self._make_lineage('sample-renewal.conf') - args = ["renew", "--dry-run", "--post-hook=no-such-command", - "--disable-hook-validation"] - self._test_renewal_common(True, [], args=args, should_renew=True, - error_expected=False) - - @mock.patch("certbot.cli.set_by_cli") - def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): - mock_set_by_cli.return_value = False - rc_path = self._make_lineage('sample-renewal-ancient.conf') - args = mock.MagicMock(account=None, email=None, webroot_path=None) - config = configuration.NamespaceConfig(args) - lineage = storage.RenewableCert(rc_path, - configuration.RenewerConfiguration(config)) - renewalparams = lineage.configuration["renewalparams"] - # pylint: disable=protected-access - renewal._restore_webroot_config(config, renewalparams) - self.assertEqual(config.webroot_path, ["/var/www/"]) - - def test_renew_verb_empty_config(self): - rd = os.path.join(self.config_dir, 'renewal') - if not os.path.exists(rd): - os.makedirs(rd) - with open(os.path.join(rd, 'empty.conf'), 'w'): - pass # leave the file empty - args = ["renew", "--dry-run", "-tvv"] - self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True) - - def _make_dummy_renewal_config(self): - renewer_configs_dir = os.path.join(self.config_dir, 'renewal') - os.makedirs(renewer_configs_dir) - with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f: - f.write("My contents don't matter") - - def _test_renew_common(self, renewalparams=None, names=None, - assert_oc_called=None, **kwargs): - self._make_dummy_renewal_config() - with mock.patch('certbot.storage.RenewableCert') as mock_rc: - mock_lineage = mock.MagicMock() - mock_lineage.fullchain = "somepath/fullchain.pem" - if renewalparams is not None: - mock_lineage.configuration = {'renewalparams': renewalparams} - if names is not None: - mock_lineage.names.return_value = names - mock_rc.return_value = mock_lineage - with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert: - kwargs.setdefault('args', ['renew']) - self._test_renewal_common(True, None, should_renew=False, **kwargs) - - if assert_oc_called is not None: - if assert_oc_called: - self.assertTrue(mock_obtain_cert.called) - else: - self.assertFalse(mock_obtain_cert.called) - - def test_renew_no_renewalparams(self): - self._test_renew_common(assert_oc_called=False, error_expected=True) - - def test_renew_no_authenticator(self): - self._test_renew_common(renewalparams={}, assert_oc_called=False, - error_expected=True) - - def test_renew_with_bad_int(self): - renewalparams = {'authenticator': 'webroot', - 'rsa_key_size': 'over 9000'} - self._test_renew_common(renewalparams=renewalparams, error_expected=True, - assert_oc_called=False) - - def test_renew_with_nonetype_http01(self): - renewalparams = {'authenticator': 'webroot', - 'http01_port': 'None'} - self._test_renew_common(renewalparams=renewalparams, - assert_oc_called=True) - - def test_renew_with_bad_domain(self): - renewalparams = {'authenticator': 'webroot'} - names = ['*.example.com'] - self._test_renew_common(renewalparams=renewalparams, error_expected=True, - names=names, assert_oc_called=False) - - def test_renew_with_configurator(self): - renewalparams = {'authenticator': 'webroot'} - self._test_renew_common( - renewalparams=renewalparams, assert_oc_called=True, - args='renew --configurator apache'.split()) - - def test_renew_plugin_config_restoration(self): - renewalparams = {'authenticator': 'webroot', - 'webroot_path': 'None', - 'webroot_imaginary_flag': '42'} - self._test_renew_common(renewalparams=renewalparams, - assert_oc_called=True) - - def test_renew_with_webroot_map(self): - renewalparams = {'authenticator': 'webroot'} - self._test_renew_common( - renewalparams=renewalparams, assert_oc_called=True, - args=['renew', '--webroot-map', '{"example.com": "/tmp"}']) - - def test_renew_reconstitute_error(self): - # pylint: disable=protected-access - with mock.patch('certbot.main.renewal._reconstitute') as mock_reconstitute: - mock_reconstitute.side_effect = Exception - self._test_renew_common(assert_oc_called=False, error_expected=True) - - def test_renew_obtain_cert_error(self): - self._make_dummy_renewal_config() - with mock.patch('certbot.storage.RenewableCert') as mock_rc: - mock_lineage = mock.MagicMock() - mock_lineage.fullchain = "somewhere/fullchain.pem" - mock_rc.return_value = mock_lineage - mock_lineage.configuration = { - 'renewalparams': {'authenticator': 'webroot'}} - with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert: - mock_obtain_cert.side_effect = Exception - self._test_renewal_common(True, None, error_expected=True, - args=['renew'], should_renew=False) - - def test_renew_with_bad_cli_args(self): - self._test_renewal_common(True, None, args='renew -d example.com'.split(), - should_renew=False, error_expected=True) - self._test_renewal_common(True, None, args='renew --csr {0}'.format(CSR).split(), - should_renew=False, error_expected=True) - - @mock.patch('certbot.main.zope.component.getUtility') - @mock.patch('certbot.main._treat_as_renewal') - @mock.patch('certbot.main._init_le_client') - def test_certonly_reinstall(self, mock_init, mock_renewal, mock_get_utility): - mock_renewal.return_value = ('reinstall', mock.MagicMock()) - mock_init.return_value = mock_client = mock.MagicMock() - self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly']) - self.assertFalse(mock_client.obtain_certificate.called) - self.assertFalse(mock_client.obtain_and_enroll_certificate.called) - self.assertEqual(mock_get_utility().add_message.call_count, 0) - #self.assertTrue('donate' not in mock_get_utility().add_message.call_args[0][0]) - - def _test_certonly_csr_common(self, extra_args=None): - certr = 'certr' - chain = 'chain' - mock_client = mock.MagicMock() - mock_client.obtain_certificate_from_csr.return_value = (certr, chain) - cert_path = '/etc/letsencrypt/live/example.com/cert.pem' - mock_client.save_certificate.return_value = cert_path, None, None - with mock.patch('certbot.main._init_le_client') as mock_init: - mock_init.return_value = mock_client - get_utility_path = 'certbot.main.zope.component.getUtility' - with mock.patch(get_utility_path) as mock_get_utility: - chain_path = '/etc/letsencrypt/live/example.com/chain.pem' - full_path = '/etc/letsencrypt/live/example.com/fullchain.pem' - args = ('-a standalone certonly --csr {0} --cert-path {1} ' - '--chain-path {2} --fullchain-path {3}').format( - CSR, cert_path, chain_path, full_path).split() - if extra_args: - args += extra_args - with mock.patch('certbot.main.crypto_util'): - self._call(args) - - if '--dry-run' in args: - self.assertFalse(mock_client.save_certificate.called) - else: - mock_client.save_certificate.assert_called_once_with( - certr, chain, cert_path, chain_path, full_path) - - return mock_get_utility - - def test_certonly_csr(self): - mock_get_utility = self._test_certonly_csr_common() - cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] - self.assertTrue('cert.pem' in cert_msg) - self.assertTrue( - 'donate' in mock_get_utility().add_message.call_args[0][0]) - - def test_certonly_csr_dry_run(self): - mock_get_utility = self._test_certonly_csr_common(['--dry-run']) - self.assertEqual(mock_get_utility().add_message.call_count, 1) - self.assertTrue( - 'dry run' in mock_get_utility().add_message.call_args[0][0]) - - @mock.patch('certbot.main.client.acme_client') - def test_revoke_with_key(self, mock_acme_client): - server = 'foo.bar' - self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY, - '--server', server, 'revoke']) - with open(KEY, 'rb') as f: - mock_acme_client.Client.assert_called_once_with( - server, key=jose.JWK.load(f.read()), net=mock.ANY) - with open(CERT, 'rb') as f: - cert = crypto_util.pyopenssl_load_certificate(f.read())[0] - mock_revoke = mock_acme_client.Client().revoke - mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) - - @mock.patch('certbot.main._determine_account') - def test_revoke_without_key(self, mock_determine_account): - mock_determine_account.return_value = (mock.MagicMock(), None) - _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) - with open(CERT) as f: - cert = crypto_util.pyopenssl_load_certificate(f.read())[0] - mock_revoke = client.acme_from_config_key().revoke - mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) - - @mock.patch('certbot.main.sys') - def test_handle_exception(self, mock_sys): - # pylint: disable=protected-access - from acme import messages - - config = mock.MagicMock() - mock_open = mock.mock_open() - - with mock.patch('certbot.main.open', mock_open, create=True): - exception = Exception('detail') - config.verbose_count = 1 - main._handle_exception( - Exception, exc_value=exception, trace=None, config=None) - mock_open().write.assert_any_call(''.join( - traceback.format_exception_only(Exception, exception))) - error_msg = mock_sys.exit.call_args_list[0][0][0] - self.assertTrue('unexpected error' in error_msg) - - with mock.patch('certbot.main.open', mock_open, create=True): - mock_open.side_effect = [KeyboardInterrupt] - error = errors.Error('detail') - main._handle_exception( - errors.Error, exc_value=error, trace=None, config=None) - # assert_any_call used because sys.exit doesn't exit in cli.py - mock_sys.exit.assert_any_call(''.join( - traceback.format_exception_only(errors.Error, error))) - - bad_typ = messages.ERROR_PREFIX + 'triffid' - exception = messages.Error(detail='alpha', typ=bad_typ, title='beta') - config = mock.MagicMock(debug=False, verbose_count=-3) - main._handle_exception( - messages.Error, exc_value=exception, trace=None, config=config) - error_msg = mock_sys.exit.call_args_list[-1][0][0] - self.assertTrue('unexpected error' in error_msg) - self.assertTrue('acme:error' not in error_msg) - self.assertTrue('alpha' in error_msg) - self.assertTrue('beta' in error_msg) - config = mock.MagicMock(debug=False, verbose_count=1) - main._handle_exception( - messages.Error, exc_value=exception, trace=None, config=config) - error_msg = mock_sys.exit.call_args_list[-1][0][0] - self.assertTrue('unexpected error' in error_msg) - self.assertTrue('acme:error' in error_msg) - self.assertTrue('alpha' in error_msg) - - interrupt = KeyboardInterrupt('detail') - main._handle_exception( - KeyboardInterrupt, exc_value=interrupt, trace=None, config=None) - mock_sys.exit.assert_called_with(''.join( - traceback.format_exception_only(KeyboardInterrupt, interrupt))) - - def test_read_file(self): - rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo')) - self.assertRaises( - argparse.ArgumentTypeError, cli.read_file, rel_test_path) - - test_contents = b'bar\n' - with open(rel_test_path, 'wb') as f: - f.write(test_contents) - - path, contents = cli.read_file(rel_test_path) - self.assertEqual(path, os.path.abspath(path)) - self.assertEqual(contents, test_contents) - - def test_agree_dev_preview_config(self): - with mock.patch('certbot.main.run') as mocked_run: - self._call(['-c', test_util.vector_path('cli.ini')]) - self.assertTrue(mocked_run.called) - - def test_register(self): - with mock.patch('certbot.main.client') as mocked_client: - acc = mock.MagicMock() - acc.id = "imaginary_account" - mocked_client.register.return_value = (acc, "worked") - self._call_no_clientmock(["register", "--email", "user@example.org"]) - # TODO: It would be more correct to explicitly check that - # _determine_account() gets called in the above case, - # but coverage statistics should also show that it did. - with mock.patch('certbot.main.account') as mocked_account: - mocked_storage = mock.MagicMock() - mocked_account.AccountFileStorage.return_value = mocked_storage - mocked_storage.find_all.return_value = ["an account"] - x = self._call_no_clientmock(["register", "--email", "user@example.org"]) - self.assertTrue("There is an existing account" in x[0]) - - def test_update_registration_no_existing_accounts(self): - # with mock.patch('certbot.main.client') as mocked_client: - with mock.patch('certbot.main.account') as mocked_account: - mocked_storage = mock.MagicMock() - mocked_account.AccountFileStorage.return_value = mocked_storage - mocked_storage.find_all.return_value = [] - x = self._call_no_clientmock( - ["register", "--update-registration", "--email", - "user@example.org"]) - self.assertTrue("Could not find an existing account" in x[0]) - - def test_update_registration_unsafely(self): - # This test will become obsolete when register --update-registration - # supports removing an e-mail address from the account - with mock.patch('certbot.main.account') as mocked_account: - mocked_storage = mock.MagicMock() - mocked_account.AccountFileStorage.return_value = mocked_storage - mocked_storage.find_all.return_value = ["an account"] - x = self._call_no_clientmock( - "register --update-registration " - "--register-unsafely-without-email".split()) - self.assertTrue("--register-unsafely-without-email" in x[0]) - - @mock.patch('certbot.main.display_ops.get_email') - @mock.patch('certbot.main.zope.component.getUtility') - def test_update_registration_with_email(self, mock_utility, mock_email): - email = "user@example.com" - mock_email.return_value = email - with mock.patch('certbot.main.client') as mocked_client: - with mock.patch('certbot.main.account') as mocked_account: - with mock.patch('certbot.main._determine_account') as mocked_det: - with mock.patch('certbot.main.client') as mocked_client: - mocked_storage = mock.MagicMock() - mocked_account.AccountFileStorage.return_value = mocked_storage - mocked_storage.find_all.return_value = ["an account"] - mocked_det.return_value = (mock.MagicMock(), "foo") - acme_client = mock.MagicMock() - mocked_client.Client.return_value = acme_client - x = self._call_no_clientmock( - ["register", "--update-registration"]) - # When registration change succeeds, the return value - # of register() is None - self.assertTrue(x[0] is None) - # and we got supposedly did update the registration from - # the server - self.assertTrue( - acme_client.acme.update_registration.called) - # and we saved the updated registration on disk - self.assertTrue(mocked_storage.save_regr.called) - self.assertTrue( - email in mock_utility().add_message.call_args[0][0]) - -class DetermineAccountTest(unittest.TestCase): - """Tests for certbot.cli._determine_account.""" - - def setUp(self): - self.args = mock.MagicMock(account=None, email=None, - register_unsafely_without_email=False) - self.config = configuration.NamespaceConfig(self.args) - self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')] - self.account_storage = account.AccountMemoryStorage() - - def _call(self): - # pylint: disable=protected-access - from certbot.main import _determine_account - with mock.patch('certbot.main.account.AccountFileStorage') as mock_storage: - mock_storage.return_value = self.account_storage - return _determine_account(self.config) - - def test_args_account_set(self): - self.account_storage.save(self.accs[1]) - self.config.account = self.accs[1].id - self.assertEqual((self.accs[1], None), self._call()) - self.assertEqual(self.accs[1].id, self.config.account) - self.assertTrue(self.config.email is None) - - def test_single_account(self): - self.account_storage.save(self.accs[0]) - self.assertEqual((self.accs[0], None), self._call()) - self.assertEqual(self.accs[0].id, self.config.account) - self.assertTrue(self.config.email is None) - - @mock.patch('certbot.client.display_ops.choose_account') - def test_multiple_accounts(self, mock_choose_accounts): - for acc in self.accs: - self.account_storage.save(acc) - mock_choose_accounts.return_value = self.accs[1] - self.assertEqual((self.accs[1], None), self._call()) - self.assertEqual( - set(mock_choose_accounts.call_args[0][0]), set(self.accs)) - self.assertEqual(self.accs[1].id, self.config.account) - self.assertTrue(self.config.email is None) - - @mock.patch('certbot.client.display_ops.get_email') - def test_no_accounts_no_email(self, mock_get_email): - mock_get_email.return_value = 'foo@bar.baz' - - with mock.patch('certbot.main.client') as client: - client.register.return_value = ( - self.accs[0], mock.sentinel.acme) - self.assertEqual((self.accs[0], mock.sentinel.acme), self._call()) - client.register.assert_called_once_with( - self.config, self.account_storage, tos_cb=mock.ANY) - - self.assertEqual(self.accs[0].id, self.config.account) - self.assertEqual('foo@bar.baz', self.config.email) - - def test_no_accounts_email(self): - self.config.email = 'other email' - with mock.patch('certbot.main.client') as client: - client.register.return_value = (self.accs[1], mock.sentinel.acme) - self._call() - self.assertEqual(self.accs[1].id, self.config.account) - self.assertEqual('other email', self.config.email) - - -class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): - """Test to avoid duplicate lineages.""" - - def setUp(self): - super(DuplicativeCertsTest, self).setUp() - self.config.write() - self._write_out_ex_kinds() - - def tearDown(self): - shutil.rmtree(self.tempdir) - - @mock.patch('certbot.util.make_or_verify_dir') - def test_find_duplicative_names(self, unused_makedir): - from certbot.main import _find_duplicative_certs - test_cert = test_util.load_vector('cert-san.pem') - with open(self.test_rc.cert, 'wb') as f: - f.write(test_cert) - - # No overlap at all - result = _find_duplicative_certs( - self.cli_config, ['wow.net', 'hooray.org']) - self.assertEqual(result, (None, None)) - - # Totally identical - result = _find_duplicative_certs( - self.cli_config, ['example.com', 'www.example.com']) - self.assertTrue(result[0].configfile.filename.endswith('example.org.conf')) - self.assertEqual(result[1], None) - - # Superset - result = _find_duplicative_certs( - self.cli_config, ['example.com', 'www.example.com', 'something.new']) - self.assertEqual(result[0], None) - self.assertTrue(result[1].configfile.filename.endswith('example.org.conf')) - - # Partial overlap doesn't count - result = _find_duplicative_certs( - self.cli_config, ['example.com', 'something.new']) - self.assertEqual(result, (None, None)) + config_dir_option = 'config_dir' + self.assertFalse(cli.option_was_set( + config_dir_option, cli.flag_default(config_dir_option))) class DefaultTest(unittest.TestCase): diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index d79fb1852..b9f5cf15b 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -13,7 +13,7 @@ from certbot import account from certbot import errors from certbot import util -from certbot.tests import test_util +import certbot.tests.util as test_util KEY = test_util.load_vector("rsa512_key.pem") diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 4832e2869..a580574a4 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -11,7 +11,7 @@ import zope.component from certbot import errors from certbot import interfaces from certbot import util -from certbot.tests import test_util +import certbot.tests.util as test_util RSA256_KEY = test_util.load_vector('rsa256_key.pem') diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 44efcf703..bcef6088b 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -17,7 +17,7 @@ from certbot import interfaces from certbot.display import util as display_util -from certbot.tests import test_util +import certbot.tests.util as test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index fa60d07b8..a548377bd 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -7,6 +7,7 @@ import unittest import mock + def get_signals(signums): """Get the handlers for an iterable of signums.""" return dict((s, signal.getsignal(s)) for s in signums) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index ea744b570..68e5ef00a 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1,35 +1,48 @@ """Tests for certbot.main.""" +from __future__ import print_function + +import itertools +import mock import os import shutil import tempfile +import traceback import unittest import datetime import pytz -import mock +import six +from acme import jose + +from certbot import account from certbot import cli from certbot import colored_logging from certbot import constants from certbot import configuration +from certbot import crypto_util from certbot import errors -from certbot.plugins import disco as plugins_disco +from certbot import main +from certbot import renewal +from certbot import storage +from certbot import util -from certbot.tests import test_util -from acme import jose +from certbot.plugins import disco +from certbot.plugins import manual + +from certbot.tests import storage_test +import certbot.tests.util as test_util CERT_PATH = test_util.vector_path('cert.pem') -KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem")) +CERT = test_util.vector_path('cert.pem') +CSR = test_util.vector_path('csr.der') +KEY = test_util.vector_path('rsa256_key.pem') +JWK = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem")) -class MainTest(unittest.TestCase): - def setUp(self): - pass - - def tearDown(self): - pass +class TestHandleIdenticalCerts(unittest.TestCase): + """Test for certbot.main._handle_identical_cert_request""" def test_handle_identical_cert_request_pending(self): - from certbot import main mock_lineage = mock.Mock() mock_lineage.ensure_deployed.return_value = False # pylint: disable=protected-access @@ -61,7 +74,7 @@ class RunTest(unittest.TestCase): def _call(self): args = '-a webroot -i null -d {0}'.format(self.domain).split() - plugins = plugins_disco.PluginsRegistry.find_all() + plugins = disco.PluginsRegistry.find_all() config = configuration.NamespaceConfig( cli.prepare_and_parse_args(plugins, args)) @@ -96,11 +109,10 @@ class ObtainCertTest(unittest.TestCase): self.get_utility_patch.stop() def _call(self, args): - plugins = plugins_disco.PluginsRegistry.find_all() + plugins = disco.PluginsRegistry.find_all() config = configuration.NamespaceConfig( cli.prepare_and_parse_args(plugins, args)) - from certbot import main with mock.patch('certbot.main._init_le_client') as mock_init: main.obtain_cert(config, plugins) @@ -117,6 +129,7 @@ class ObtainCertTest(unittest.TestCase): # pylint: disable=unused-argument self.assertFalse(pause) + class RevokeTest(unittest.TestCase): """Tests for certbot.main.revoke.""" @@ -144,11 +157,10 @@ class RevokeTest(unittest.TestCase): creation_host="test.certbot.org", creation_dt=datetime.datetime( 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC)) - self.acc = Account(self.regr, KEY, self.meta) + self.acc = Account(self.regr, JWK, self.meta) self.mock_determine_account.return_value = (self.acc, None) - def tearDown(self): shutil.rmtree(self.tempdir_path) for patch in self.patches: @@ -156,7 +168,7 @@ class RevokeTest(unittest.TestCase): def _call(self): args = 'revoke --cert-path={0}'.format(self.tmp_cert_path).split() - plugins = plugins_disco.PluginsRegistry.find_all() + plugins = disco.PluginsRegistry.find_all() config = configuration.NamespaceConfig( cli.prepare_and_parse_args(plugins, args)) @@ -173,6 +185,7 @@ class RevokeTest(unittest.TestCase): self.assertRaises(acme_errors.ClientError, self._call) self.mock_success_revoke.assert_not_called() + class SetupLogFileHandlerTest(unittest.TestCase): """Tests for certbot.main.setup_log_file_handler.""" @@ -256,5 +269,884 @@ class MakeOrVerifyCoreDirTest(unittest.TestCase): self.dir, 0o700, os.geteuid(), False) +class DetermineAccountTest(unittest.TestCase): + """Tests for certbot.main._determine_account.""" + + def setUp(self): + self.args = mock.MagicMock(account=None, email=None, + register_unsafely_without_email=False) + self.config = configuration.NamespaceConfig(self.args) + self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')] + self.account_storage = account.AccountMemoryStorage() + + def _call(self): + # pylint: disable=protected-access + from certbot.main import _determine_account + with mock.patch('certbot.main.account.AccountFileStorage') as mock_storage: + mock_storage.return_value = self.account_storage + return _determine_account(self.config) + + def test_args_account_set(self): + self.account_storage.save(self.accs[1]) + self.config.account = self.accs[1].id + self.assertEqual((self.accs[1], None), self._call()) + self.assertEqual(self.accs[1].id, self.config.account) + self.assertTrue(self.config.email is None) + + def test_single_account(self): + self.account_storage.save(self.accs[0]) + self.assertEqual((self.accs[0], None), self._call()) + self.assertEqual(self.accs[0].id, self.config.account) + self.assertTrue(self.config.email is None) + + @mock.patch('certbot.client.display_ops.choose_account') + def test_multiple_accounts(self, mock_choose_accounts): + for acc in self.accs: + self.account_storage.save(acc) + mock_choose_accounts.return_value = self.accs[1] + self.assertEqual((self.accs[1], None), self._call()) + self.assertEqual( + set(mock_choose_accounts.call_args[0][0]), set(self.accs)) + self.assertEqual(self.accs[1].id, self.config.account) + self.assertTrue(self.config.email is None) + + @mock.patch('certbot.client.display_ops.get_email') + def test_no_accounts_no_email(self, mock_get_email): + mock_get_email.return_value = 'foo@bar.baz' + + with mock.patch('certbot.main.client') as client: + client.register.return_value = ( + self.accs[0], mock.sentinel.acme) + self.assertEqual((self.accs[0], mock.sentinel.acme), self._call()) + client.register.assert_called_once_with( + self.config, self.account_storage, tos_cb=mock.ANY) + + self.assertEqual(self.accs[0].id, self.config.account) + self.assertEqual('foo@bar.baz', self.config.email) + + def test_no_accounts_email(self): + self.config.email = 'other email' + with mock.patch('certbot.main.client') as client: + client.register.return_value = (self.accs[1], mock.sentinel.acme) + self._call() + self.assertEqual(self.accs[1].id, self.config.account) + self.assertEqual('other email', self.config.email) + + +class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): + """Test to avoid duplicate lineages.""" + + def setUp(self): + super(DuplicativeCertsTest, self).setUp() + self.config.write() + self._write_out_ex_kinds() + + def tearDown(self): + shutil.rmtree(self.tempdir) + + @mock.patch('certbot.util.make_or_verify_dir') + def test_find_duplicative_names(self, unused_makedir): + from certbot.main import _find_duplicative_certs + test_cert = test_util.load_vector('cert-san.pem') + with open(self.test_rc.cert, 'wb') as f: + f.write(test_cert) + + # No overlap at all + result = _find_duplicative_certs( + self.cli_config, ['wow.net', 'hooray.org']) + self.assertEqual(result, (None, None)) + + # Totally identical + result = _find_duplicative_certs( + self.cli_config, ['example.com', 'www.example.com']) + self.assertTrue(result[0].configfile.filename.endswith('example.org.conf')) + self.assertEqual(result[1], None) + + # Superset + result = _find_duplicative_certs( + self.cli_config, ['example.com', 'www.example.com', 'something.new']) + self.assertEqual(result[0], None) + self.assertTrue(result[1].configfile.filename.endswith('example.org.conf')) + + # Partial overlap doesn't count + result = _find_duplicative_certs( + self.cli_config, ['example.com', 'something.new']) + self.assertEqual(result, (None, None)) + + +class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods + """Tests for different commands.""" + + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + self.config_dir = os.path.join(self.tmp_dir, 'config') + self.work_dir = os.path.join(self.tmp_dir, 'work') + self.logs_dir = os.path.join(self.tmp_dir, 'logs') + os.mkdir(self.logs_dir) + self.standard_args = ['--config-dir', self.config_dir, + '--work-dir', self.work_dir, + '--logs-dir', self.logs_dir, '--text'] + + def tearDown(self): + shutil.rmtree(self.tmp_dir) + # Reset globals in cli + # pylint: disable=protected-access + cli._parser = cli.set_by_cli.detector = None + + def _call(self, args, stdout=None): + "Run the cli with output streams and actual client mocked out" + with mock.patch('certbot.main.client') as client: + ret, stdout, stderr = self._call_no_clientmock(args, stdout) + return ret, stdout, stderr, client + + def _call_no_clientmock(self, args, stdout=None): + "Run the client with output streams mocked out" + args = self.standard_args + args + + toy_stdout = stdout if stdout else six.StringIO() + with mock.patch('certbot.main.sys.stdout', new=toy_stdout): + with mock.patch('certbot.main.sys.stderr') as stderr: + ret = main.main(args[:]) # NOTE: parser can alter its args! + return ret, toy_stdout, stderr + + def test_no_flags(self): + with mock.patch('certbot.main.run') as mock_run: + self._call([]) + self.assertEqual(1, mock_run.call_count) + + def test_version_string_program_name(self): + toy_out = six.StringIO() + toy_err = six.StringIO() + with mock.patch('certbot.main.sys.stdout', new=toy_out): + with mock.patch('certbot.main.sys.stderr', new=toy_err): + try: + main.main(["--version"]) + except SystemExit: + pass + finally: + output = toy_out.getvalue() or toy_err.getvalue() + self.assertTrue("certbot" in output, "Output is {0}".format(output)) + toy_out.close() + toy_err.close() + + def _cli_missing_flag(self, args, message): + "Ensure that a particular error raises a missing cli flag error containing message" + exc = None + try: + with mock.patch('certbot.main.sys.stderr'): + main.main(self.standard_args + args[:]) # NOTE: parser can alter its args! + except errors.MissingCommandlineFlag as exc_: + exc = exc_ + self.assertTrue(message in str(exc)) + self.assertTrue(exc is not None) + + def test_noninteractive(self): + args = ['-n', 'certonly'] + self._cli_missing_flag(args, "specify a plugin") + args.extend(['--standalone', '-d', 'eg.is']) + self._cli_missing_flag(args, "register before running") + with mock.patch('certbot.main._auth_from_domains'): + with mock.patch('certbot.main.client.acme_from_config_key'): + args.extend(['--email', 'io@io.is']) + self._cli_missing_flag(args, "--agree-tos") + + @mock.patch('certbot.main.client.acme_client.Client') + @mock.patch('certbot.main._determine_account') + @mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate') + @mock.patch('certbot.main._auth_from_domains') + def test_user_agent(self, afd, _obt, det, _client): + # Normally the client is totally mocked out, but here we need more + # arguments to automate it... + args = ["--standalone", "certonly", "-m", "none@none.com", + "-d", "example.com", '--agree-tos'] + self.standard_args + det.return_value = mock.MagicMock(), None + afd.return_value = "newcert", mock.MagicMock() + + with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: + self._call_no_clientmock(args) + os_ver = util.get_os_info_ua() + ua = acme_net.call_args[1]["user_agent"] + self.assertTrue(os_ver in ua) + import platform + plat = platform.platform() + if "linux" in plat.lower(): + self.assertTrue(util.get_os_info_ua() in ua) + + with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: + ua = "bandersnatch" + args += ["--user-agent", ua] + self._call_no_clientmock(args) + acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) + + @mock.patch('certbot.main.plug_sel.record_chosen_plugins') + @mock.patch('certbot.main.plug_sel.pick_installer') + def test_installer_selection(self, mock_pick_installer, _rec): + self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', + '--key-path', 'key', '--chain-path', 'chain']) + self.assertEqual(mock_pick_installer.call_count, 1) + + @mock.patch('certbot.util.exe_exists') + def test_configurator_selection(self, mock_exe_exists): + mock_exe_exists.return_value = True + real_plugins = disco.PluginsRegistry.find_all() + args = ['--apache', '--authenticator', 'standalone'] + + # This needed two calls to find_all(), which we're avoiding for now + # because of possible side effects: + # https://github.com/letsencrypt/letsencrypt/commit/51ed2b681f87b1eb29088dd48718a54f401e4855 + #with mock.patch('certbot.cli.plugins_testable') as plugins: + # plugins.return_value = {"apache": True, "nginx": True} + # ret, _, _, _ = self._call(args) + # self.assertTrue("Too many flags setting" in ret) + + args = ["install", "--nginx", "--cert-path", "/tmp/blah", "--key-path", "/tmp/blah", + "--nginx-server-root", "/nonexistent/thing", "-d", + "example.com", "--debug"] + if "nginx" in real_plugins: + # Sending nginx a non-existent conf dir will simulate misconfiguration + # (we can only do that if certbot-nginx is actually present) + ret, _, _, _ = self._call(args) + self.assertTrue("The nginx plugin is not working" in ret) + self.assertTrue("MisconfigurationError" in ret) + + self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") + + with mock.patch("certbot.main._init_le_client") as mock_init: + with mock.patch("certbot.main._auth_from_domains") as mock_afd: + mock_afd.return_value = (mock.MagicMock(), mock.MagicMock()) + self._call(["certonly", "--manual", "-d", "foo.bar"]) + unused_config, auth, unused_installer = mock_init.call_args[0] + self.assertTrue(isinstance(auth, manual.Authenticator)) + + with mock.patch('certbot.main.obtain_cert') as mock_certonly: + self._call(["auth", "--standalone"]) + self.assertEqual(1, mock_certonly.call_count) + + def test_rollback(self): + _, _, _, client = self._call(['rollback']) + self.assertEqual(1, client.rollback.call_count) + + _, _, _, client = self._call(['rollback', '--checkpoints', '123']) + client.rollback.assert_called_once_with( + mock.ANY, 123, mock.ANY, mock.ANY) + + def test_config_changes(self): + _, _, _, client = self._call(['config_changes']) + self.assertEqual(1, client.view_config_changes.call_count) + + @mock.patch('certbot.cert_manager.update_live_symlinks') + def test_update_symlinks(self, mock_cert_manager): + self._call_no_clientmock(['update_symlinks']) + self.assertEqual(1, mock_cert_manager.call_count) + + @mock.patch('certbot.cert_manager.certificates') + def test_certificates(self, mock_cert_manager): + self._call_no_clientmock(['certificates']) + self.assertEqual(1, mock_cert_manager.call_count) + + def test_plugins(self): + flags = ['--init', '--prepare', '--authenticators', '--installers'] + for args in itertools.chain( + *(itertools.combinations(flags, r) + for r in six.moves.range(len(flags)))): + self._call(['plugins'] + list(args)) + + @mock.patch('certbot.main.plugins_disco') + @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics') + def test_plugins_no_args(self, _det, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + + _, stdout, _, _ = self._call(['plugins']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + self.assertEqual(stdout.getvalue().strip(), str(filtered)) + + @mock.patch('certbot.main.plugins_disco') + @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics') + def test_plugins_init(self, _det, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + + _, stdout, _, _ = self._call(['plugins', '--init']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + self.assertEqual(filtered.init.call_count, 1) + filtered.verify.assert_called_once_with(ifaces) + verified = filtered.verify() + self.assertEqual(stdout.getvalue().strip(), str(verified)) + + @mock.patch('certbot.main.plugins_disco') + @mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics') + def test_plugins_prepare(self, _det, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + _, stdout, _, _ = self._call(['plugins', '--init', '--prepare']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + self.assertEqual(filtered.init.call_count, 1) + filtered.verify.assert_called_once_with(ifaces) + verified = filtered.verify() + verified.prepare.assert_called_once_with() + verified.available.assert_called_once_with() + available = verified.available() + self.assertEqual(stdout.getvalue().strip(), str(available)) + + def test_certonly_abspath(self): + cert = 'cert' + key = 'key' + chain = 'chain' + fullchain = 'fullchain' + + with mock.patch('certbot.main.obtain_cert') as mock_obtaincert: + self._call(['certonly', '--cert-path', cert, '--key-path', 'key', + '--chain-path', 'chain', + '--fullchain-path', 'fullchain']) + + config, unused_plugins = mock_obtaincert.call_args[0] + self.assertEqual(config.cert_path, os.path.abspath(cert)) + self.assertEqual(config.key_path, os.path.abspath(key)) + self.assertEqual(config.chain_path, os.path.abspath(chain)) + self.assertEqual(config.fullchain_path, os.path.abspath(fullchain)) + + def test_certonly_bad_args(self): + try: + self._call(['-a', 'bad_auth', 'certonly']) + assert False, "Exception should have been raised" + except errors.PluginSelectionError as e: + self.assertTrue('The requested bad_auth plugin does not appear' in str(e)) + + def test_check_config_sanity_domain(self): + # FQDN + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', 'a' * 64]) + # FQDN 2 + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', (('a' * 50) + '.') * 10]) + # Wildcard + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', '*.wildcard.tld']) + + # Bare IP address (this is actually a different error message now) + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', '204.11.231.35']) + + def test_csr_with_besteffort(self): + self.assertRaises( + errors.Error, self._call, + 'certonly --csr {0} --allow-subset-of-names'.format(CSR).split()) + + def test_run_with_csr(self): + # This is an error because you can only use --csr with certonly + try: + self._call(['--csr', CSR]) + except errors.Error as e: + assert "Please try the certonly" in repr(e) + return + assert False, "Expected supplying --csr to fail with default verb" + + def test_csr_with_no_domains(self): + self.assertRaises( + errors.Error, self._call, + 'certonly --csr {0}'.format( + test_util.vector_path('csr-nonames.pem')).split()) + + def test_csr_with_inconsistent_domains(self): + self.assertRaises( + errors.Error, self._call, + 'certonly -d example.org --csr {0}'.format(CSR).split()) + + def _certonly_new_request_common(self, mock_client, args=None): + with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: + mock_renewal.return_value = ("newcert", None) + with mock.patch('certbot.main._init_le_client') as mock_init: + mock_init.return_value = mock_client + if args is None: + args = [] + args += '-d foo.bar -a standalone certonly'.split() + self._call(args) + + @mock.patch('certbot.main.zope.component.getUtility') + def test_certonly_dry_run_new_request_success(self, mock_get_utility): + mock_client = mock.MagicMock() + mock_client.obtain_and_enroll_certificate.return_value = None + self._certonly_new_request_common(mock_client, ['--dry-run']) + self.assertEqual( + mock_client.obtain_and_enroll_certificate.call_count, 1) + self.assertTrue( + 'dry run' in mock_get_utility().add_message.call_args[0][0]) + # Asserts we don't suggest donating after a successful dry run + self.assertEqual(mock_get_utility().add_message.call_count, 1) + + @mock.patch('certbot.crypto_util.notAfter') + @mock.patch('certbot.main.zope.component.getUtility') + def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): + cert_path = '/etc/letsencrypt/live/foo.bar' + date = '1970-01-01' + mock_notAfter().date.return_value = date + + mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path) + mock_client = mock.MagicMock() + mock_client.obtain_and_enroll_certificate.return_value = mock_lineage + self._certonly_new_request_common(mock_client) + self.assertEqual( + mock_client.obtain_and_enroll_certificate.call_count, 1) + cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] + self.assertTrue(cert_path in cert_msg) + self.assertTrue(date in cert_msg) + self.assertTrue( + 'donate' in mock_get_utility().add_message.call_args[0][0]) + + def test_certonly_new_request_failure(self): + mock_client = mock.MagicMock() + mock_client.obtain_and_enroll_certificate.return_value = False + self.assertRaises(errors.Error, + self._certonly_new_request_common, mock_client) + + def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, + args=None, should_renew=True, error_expected=False): + # pylint: disable=too-many-locals,too-many-arguments + cert_path = test_util.vector_path('cert.pem') + chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' + mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path) + mock_lineage.should_autorenew.return_value = due_for_renewal + mock_lineage.has_pending_deployment.return_value = False + mock_certr = mock.MagicMock() + mock_key = mock.MagicMock(pem='pem_key') + mock_client = mock.MagicMock() + stdout = None + mock_client.obtain_certificate.return_value = (mock_certr, 'chain', + mock_key, 'csr') + try: + with mock.patch('certbot.main._find_duplicative_certs') as mock_fdc: + mock_fdc.return_value = (mock_lineage, None) + with mock.patch('certbot.main._init_le_client') as mock_init: + mock_init.return_value = mock_client + get_utility_path = 'certbot.main.zope.component.getUtility' + with mock.patch(get_utility_path) as mock_get_utility: + with mock.patch('certbot.main.renewal.OpenSSL') as mock_ssl: + mock_latest = mock.MagicMock() + mock_latest.get_issuer.return_value = "Fake fake" + mock_ssl.crypto.load_certificate.return_value = mock_latest + with mock.patch('certbot.main.renewal.crypto_util'): + if not args: + args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly'] + if extra_args: + args += extra_args + try: + ret, stdout, _, _ = self._call(args) + if ret: + print("Returned", ret) + raise AssertionError(ret) + assert not error_expected, "renewal should have errored" + except: # pylint: disable=bare-except + if not error_expected: + raise AssertionError( + "Unexpected renewal error:\n" + + traceback.format_exc()) + + if should_renew: + mock_client.obtain_certificate.assert_called_once_with(['isnot.org']) + else: + self.assertEqual(mock_client.obtain_certificate.call_count, 0) + except: + self._dump_log() + raise + finally: + if log_out: + with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: + self.assertTrue(log_out in lf.read()) + + return mock_lineage, mock_get_utility, stdout + + def test_certonly_renewal(self): + lineage, get_utility, _ = self._test_renewal_common(True, []) + self.assertEqual(lineage.save_successor.call_count, 1) + lineage.update_all_links_to.assert_called_once_with( + lineage.latest_common_version()) + cert_msg = get_utility().add_message.call_args_list[0][0][0] + self.assertTrue('fullchain.pem' in cert_msg) + self.assertTrue('donate' in get_utility().add_message.call_args[0][0]) + + def test_certonly_renewal_triggers(self): + # --dry-run should force renewal + _, get_utility, _ = self._test_renewal_common(False, ['--dry-run', '--keep'], + log_out="simulating renewal") + self.assertEqual(get_utility().add_message.call_count, 1) + self.assertTrue('dry run' in get_utility().add_message.call_args[0][0]) + + self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'], + log_out="Auto-renewal forced") + self.assertEqual(get_utility().add_message.call_count, 1) + + self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], + log_out="not yet due", should_renew=False) + + def _dump_log(self): + print("Logs:") + log_path = os.path.join(self.logs_dir, "letsencrypt.log") + if os.path.exists(log_path): + with open(log_path) as lf: + print(lf.read()) + + def test_renew_verb(self): + test_util.make_lineage(self, 'sample-renewal.conf') + args = ["renew", "--dry-run", "-tvv"] + self._test_renewal_common(True, [], args=args, should_renew=True) + + def test_quiet_renew(self): + test_util.make_lineage(self, 'sample-renewal.conf') + args = ["renew", "--dry-run"] + _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) + out = stdout.getvalue() + self.assertTrue("renew" in out) + + args = ["renew", "--dry-run", "-q"] + _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) + out = stdout.getvalue() + self.assertEqual("", out) + + def test_renew_hook_validation(self): + test_util.make_lineage(self, 'sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command"] + self._test_renewal_common(True, [], args=args, should_renew=False, + error_expected=True) + + def test_renew_no_hook_validation(self): + test_util.make_lineage(self, 'sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command", + "--disable-hook-validation"] + self._test_renewal_common(True, [], args=args, should_renew=True, + error_expected=False) + + @mock.patch("certbot.cli.set_by_cli") + def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): + mock_set_by_cli.return_value = False + rc_path = test_util.make_lineage(self, 'sample-renewal-ancient.conf') + args = mock.MagicMock(account=None, email=None, webroot_path=None) + config = configuration.NamespaceConfig(args) + lineage = storage.RenewableCert(rc_path, + configuration.RenewerConfiguration(config)) + renewalparams = lineage.configuration["renewalparams"] + # pylint: disable=protected-access + renewal._restore_webroot_config(config, renewalparams) + self.assertEqual(config.webroot_path, ["/var/www/"]) + + def test_renew_verb_empty_config(self): + rd = os.path.join(self.config_dir, 'renewal') + if not os.path.exists(rd): + os.makedirs(rd) + with open(os.path.join(rd, 'empty.conf'), 'w'): + pass # leave the file empty + args = ["renew", "--dry-run", "-tvv"] + self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True) + + def _make_dummy_renewal_config(self): + renewer_configs_dir = os.path.join(self.config_dir, 'renewal') + os.makedirs(renewer_configs_dir) + with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f: + f.write("My contents don't matter") + + def _test_renew_common(self, renewalparams=None, names=None, + assert_oc_called=None, **kwargs): + self._make_dummy_renewal_config() + with mock.patch('certbot.storage.RenewableCert') as mock_rc: + mock_lineage = mock.MagicMock() + mock_lineage.fullchain = "somepath/fullchain.pem" + if renewalparams is not None: + mock_lineage.configuration = {'renewalparams': renewalparams} + if names is not None: + mock_lineage.names.return_value = names + mock_rc.return_value = mock_lineage + with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert: + kwargs.setdefault('args', ['renew']) + self._test_renewal_common(True, None, should_renew=False, **kwargs) + + if assert_oc_called is not None: + if assert_oc_called: + self.assertTrue(mock_obtain_cert.called) + else: + self.assertFalse(mock_obtain_cert.called) + + def test_renew_no_renewalparams(self): + self._test_renew_common(assert_oc_called=False, error_expected=True) + + def test_renew_no_authenticator(self): + self._test_renew_common(renewalparams={}, assert_oc_called=False, + error_expected=True) + + def test_renew_with_bad_int(self): + renewalparams = {'authenticator': 'webroot', + 'rsa_key_size': 'over 9000'} + self._test_renew_common(renewalparams=renewalparams, error_expected=True, + assert_oc_called=False) + + def test_renew_with_nonetype_http01(self): + renewalparams = {'authenticator': 'webroot', + 'http01_port': 'None'} + self._test_renew_common(renewalparams=renewalparams, + assert_oc_called=True) + + def test_renew_with_bad_domain(self): + renewalparams = {'authenticator': 'webroot'} + names = ['*.example.com'] + self._test_renew_common(renewalparams=renewalparams, error_expected=True, + names=names, assert_oc_called=False) + + def test_renew_with_configurator(self): + renewalparams = {'authenticator': 'webroot'} + self._test_renew_common( + renewalparams=renewalparams, assert_oc_called=True, + args='renew --configurator apache'.split()) + + def test_renew_plugin_config_restoration(self): + renewalparams = {'authenticator': 'webroot', + 'webroot_path': 'None', + 'webroot_imaginary_flag': '42'} + self._test_renew_common(renewalparams=renewalparams, + assert_oc_called=True) + + def test_renew_with_webroot_map(self): + renewalparams = {'authenticator': 'webroot'} + self._test_renew_common( + renewalparams=renewalparams, assert_oc_called=True, + args=['renew', '--webroot-map', '{"example.com": "/tmp"}']) + + def test_renew_reconstitute_error(self): + # pylint: disable=protected-access + with mock.patch('certbot.main.renewal._reconstitute') as mock_reconstitute: + mock_reconstitute.side_effect = Exception + self._test_renew_common(assert_oc_called=False, error_expected=True) + + def test_renew_obtain_cert_error(self): + self._make_dummy_renewal_config() + with mock.patch('certbot.storage.RenewableCert') as mock_rc: + mock_lineage = mock.MagicMock() + mock_lineage.fullchain = "somewhere/fullchain.pem" + mock_rc.return_value = mock_lineage + mock_lineage.configuration = { + 'renewalparams': {'authenticator': 'webroot'}} + with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert: + mock_obtain_cert.side_effect = Exception + self._test_renewal_common(True, None, error_expected=True, + args=['renew'], should_renew=False) + + def test_renew_with_bad_cli_args(self): + self._test_renewal_common(True, None, args='renew -d example.com'.split(), + should_renew=False, error_expected=True) + self._test_renewal_common(True, None, args='renew --csr {0}'.format(CSR).split(), + should_renew=False, error_expected=True) + + @mock.patch('certbot.main.zope.component.getUtility') + @mock.patch('certbot.main._treat_as_renewal') + @mock.patch('certbot.main._init_le_client') + def test_certonly_reinstall(self, mock_init, mock_renewal, mock_get_utility): + mock_renewal.return_value = ('reinstall', mock.MagicMock()) + mock_init.return_value = mock_client = mock.MagicMock() + self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly']) + self.assertFalse(mock_client.obtain_certificate.called) + self.assertFalse(mock_client.obtain_and_enroll_certificate.called) + self.assertEqual(mock_get_utility().add_message.call_count, 0) + #self.assertTrue('donate' not in mock_get_utility().add_message.call_args[0][0]) + + def _test_certonly_csr_common(self, extra_args=None): + certr = 'certr' + chain = 'chain' + mock_client = mock.MagicMock() + mock_client.obtain_certificate_from_csr.return_value = (certr, chain) + cert_path = '/etc/letsencrypt/live/example.com/cert.pem' + mock_client.save_certificate.return_value = cert_path, None, None + with mock.patch('certbot.main._init_le_client') as mock_init: + mock_init.return_value = mock_client + get_utility_path = 'certbot.main.zope.component.getUtility' + with mock.patch(get_utility_path) as mock_get_utility: + chain_path = '/etc/letsencrypt/live/example.com/chain.pem' + full_path = '/etc/letsencrypt/live/example.com/fullchain.pem' + args = ('-a standalone certonly --csr {0} --cert-path {1} ' + '--chain-path {2} --fullchain-path {3}').format( + CSR, cert_path, chain_path, full_path).split() + if extra_args: + args += extra_args + with mock.patch('certbot.main.crypto_util'): + self._call(args) + + if '--dry-run' in args: + self.assertFalse(mock_client.save_certificate.called) + else: + mock_client.save_certificate.assert_called_once_with( + certr, chain, cert_path, chain_path, full_path) + + return mock_get_utility + + def test_certonly_csr(self): + mock_get_utility = self._test_certonly_csr_common() + cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] + self.assertTrue('cert.pem' in cert_msg) + self.assertTrue( + 'donate' in mock_get_utility().add_message.call_args[0][0]) + + def test_certonly_csr_dry_run(self): + mock_get_utility = self._test_certonly_csr_common(['--dry-run']) + self.assertEqual(mock_get_utility().add_message.call_count, 1) + self.assertTrue( + 'dry run' in mock_get_utility().add_message.call_args[0][0]) + + @mock.patch('certbot.main.client.acme_client') + def test_revoke_with_key(self, mock_acme_client): + server = 'foo.bar' + self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY, + '--server', server, 'revoke']) + with open(KEY, 'rb') as f: + mock_acme_client.Client.assert_called_once_with( + server, key=jose.JWK.load(f.read()), net=mock.ANY) + with open(CERT, 'rb') as f: + cert = crypto_util.pyopenssl_load_certificate(f.read())[0] + mock_revoke = mock_acme_client.Client().revoke + mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) + + @mock.patch('certbot.main._determine_account') + def test_revoke_without_key(self, mock_determine_account): + mock_determine_account.return_value = (mock.MagicMock(), None) + _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) + with open(CERT) as f: + cert = crypto_util.pyopenssl_load_certificate(f.read())[0] + mock_revoke = client.acme_from_config_key().revoke + mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) + + def test_agree_dev_preview_config(self): + with mock.patch('certbot.main.run') as mocked_run: + self._call(['-c', test_util.vector_path('cli.ini')]) + self.assertTrue(mocked_run.called) + + def test_register(self): + with mock.patch('certbot.main.client') as mocked_client: + acc = mock.MagicMock() + acc.id = "imaginary_account" + mocked_client.register.return_value = (acc, "worked") + self._call_no_clientmock(["register", "--email", "user@example.org"]) + # TODO: It would be more correct to explicitly check that + # _determine_account() gets called in the above case, + # but coverage statistics should also show that it did. + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + x = self._call_no_clientmock(["register", "--email", "user@example.org"]) + self.assertTrue("There is an existing account" in x[0]) + + def test_update_registration_no_existing_accounts(self): + # with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = [] + x = self._call_no_clientmock( + ["register", "--update-registration", "--email", + "user@example.org"]) + self.assertTrue("Could not find an existing account" in x[0]) + + def test_update_registration_unsafely(self): + # This test will become obsolete when register --update-registration + # supports removing an e-mail address from the account + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + x = self._call_no_clientmock( + "register --update-registration " + "--register-unsafely-without-email".split()) + self.assertTrue("--register-unsafely-without-email" in x[0]) + + @mock.patch('certbot.main.display_ops.get_email') + @mock.patch('certbot.main.zope.component.getUtility') + def test_update_registration_with_email(self, mock_utility, mock_email): + email = "user@example.com" + mock_email.return_value = email + with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + with mock.patch('certbot.main._determine_account') as mocked_det: + with mock.patch('certbot.main.client') as mocked_client: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + mocked_det.return_value = (mock.MagicMock(), "foo") + acme_client = mock.MagicMock() + mocked_client.Client.return_value = acme_client + x = self._call_no_clientmock( + ["register", "--update-registration"]) + # When registration change succeeds, the return value + # of register() is None + self.assertTrue(x[0] is None) + # and we got supposedly did update the registration from + # the server + self.assertTrue( + acme_client.acme.update_registration.called) + # and we saved the updated registration on disk + self.assertTrue(mocked_storage.save_regr.called) + self.assertTrue( + email in mock_utility().add_message.call_args[0][0]) + + +class TestHandleException(unittest.TestCase): + """Test main._handle_exception""" + @mock.patch('certbot.main.sys') + def test_handle_exception(self, mock_sys): + # pylint: disable=protected-access + from acme import messages + + config = mock.MagicMock() + mock_open = mock.mock_open() + + with mock.patch('certbot.main.open', mock_open, create=True): + exception = Exception('detail') + config.verbose_count = 1 + main._handle_exception( + Exception, exc_value=exception, trace=None, config=None) + mock_open().write.assert_any_call(''.join( + traceback.format_exception_only(Exception, exception))) + error_msg = mock_sys.exit.call_args_list[0][0][0] + self.assertTrue('unexpected error' in error_msg) + + with mock.patch('certbot.main.open', mock_open, create=True): + mock_open.side_effect = [KeyboardInterrupt] + error = errors.Error('detail') + main._handle_exception( + errors.Error, exc_value=error, trace=None, config=None) + # assert_any_call used because sys.exit doesn't exit in cli.py + mock_sys.exit.assert_any_call(''.join( + traceback.format_exception_only(errors.Error, error))) + + bad_typ = messages.ERROR_PREFIX + 'triffid' + exception = messages.Error(detail='alpha', typ=bad_typ, title='beta') + config = mock.MagicMock(debug=False, verbose_count=-3) + main._handle_exception( + messages.Error, exc_value=exception, trace=None, config=config) + error_msg = mock_sys.exit.call_args_list[-1][0][0] + self.assertTrue('unexpected error' in error_msg) + self.assertTrue('acme:error' not in error_msg) + self.assertTrue('alpha' in error_msg) + self.assertTrue('beta' in error_msg) + config = mock.MagicMock(debug=False, verbose_count=1) + main._handle_exception( + messages.Error, exc_value=exception, trace=None, config=config) + error_msg = mock_sys.exit.call_args_list[-1][0][0] + self.assertTrue('unexpected error' in error_msg) + self.assertTrue('acme:error' in error_msg) + self.assertTrue('alpha' in error_msg) + + interrupt = KeyboardInterrupt('detail') + main._handle_exception( + KeyboardInterrupt, exc_value=interrupt, trace=None, config=None) + mock_sys.exit.assert_called_with(''.join( + traceback.format_exception_only(KeyboardInterrupt, interrupt))) + + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py new file mode 100644 index 000000000..207b70041 --- /dev/null +++ b/certbot/tests/renewal_test.py @@ -0,0 +1,30 @@ +"""Tests for certbot.renewal""" +import os +import mock +import unittest +import tempfile + +from certbot import configuration +from certbot import storage + +from certbot.tests import util + + +class RenewalTest(unittest.TestCase): + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + self.config_dir = os.path.join(self.tmp_dir, 'config') + + @mock.patch("certbot.cli.set_by_cli") + def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): + mock_set_by_cli.return_value = False + rc_path = util.make_lineage(self, 'sample-renewal-ancient.conf') + args = mock.MagicMock(account=None, email=None, webroot_path=None) + config = configuration.NamespaceConfig(args) + lineage = storage.RenewableCert( + rc_path, configuration.RenewerConfiguration(config)) + renewalparams = lineage.configuration["renewalparams"] + # pylint: disable=protected-access + from certbot import renewal + renewal._restore_webroot_config(config, renewalparams) + self.assertEqual(config.webroot_path, ["/var/www/"]) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 46e2aff0d..17c4524e0 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -17,10 +17,10 @@ from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR -from certbot.tests import test_util +from certbot.tests import util -CERT = test_util.load_cert('cert.pem') +CERT = util.load_cert('cert.pem') def unlink_all(rc_object): @@ -363,18 +363,18 @@ class RenewableCertTests(BaseRenewableCertTest): def test_names(self): # Trying the current version - self._write_out_kind("cert", 12, test_util.load_vector("cert-san.pem")) + self._write_out_kind("cert", 12, util.load_vector("cert-san.pem")) self.assertEqual(self.test_rc.names(), ["example.com", "www.example.com"]) # Trying a non-current version - self._write_out_kind("cert", 15, test_util.load_vector("cert.pem")) + self._write_out_kind("cert", 15, util.load_vector("cert.pem")) self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) # Testing common name is listed first self._write_out_kind( - "cert", 12, test_util.load_vector("cert-5sans.pem")) + "cert", 12, util.load_vector("cert-5sans.pem")) self.assertEqual( self.test_rc.names(12), ["example.com"] + ["{0}.example.com".format(c) for c in "abcd"]) @@ -387,7 +387,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_time_interval_judgments(self, mock_datetime): """Test should_autodeploy() and should_autorenew() on the basis of expiry time windows.""" - test_cert = test_util.load_vector("cert.pem") + test_cert = util.load_vector("cert.pem") self._write_out_ex_kinds() self.test_rc.update_all_links_to(12) diff --git a/certbot/tests/test_util.py b/certbot/tests/util.py similarity index 68% rename from certbot/tests/test_util.py rename to certbot/tests/util.py index ba968511f..2c5583c24 100644 --- a/certbot/tests/test_util.py +++ b/certbot/tests/util.py @@ -5,6 +5,7 @@ """ import os import pkg_resources +import shutil import unittest from cryptography.hazmat.backends import default_backend @@ -15,6 +16,9 @@ from acme import errors from acme import jose from acme import util +from certbot import constants +from certbot import storage + def vector_path(*names): """Path to a test vector.""" @@ -111,3 +115,46 @@ def skip_unless(condition, reason): # pragma: no cover return lambda cls: cls else: return lambda cls: None + + +def make_lineage(self, testfile): + """Creates a lineage defined by testfile. + + This creates the archive, live, and renewal directories if + necessary and creates a simple lineage. + + :param str testfile: configuration file to base the lineage on + + :returns: path to the renewal conf file for the created lineage + :rtype: str + + """ + lineage_name = testfile[:-len('.conf')] + + conf_dir = os.path.join( + self.config_dir, constants.RENEWAL_CONFIGS_DIR) + archive_dir = os.path.join( + self.config_dir, constants.ARCHIVE_DIR, lineage_name) + live_dir = os.path.join( + self.config_dir, constants.LIVE_DIR, lineage_name) + + for directory in (archive_dir, conf_dir, live_dir,): + if not os.path.exists(directory): + os.makedirs(directory) + + sample_archive = vector_path('sample-archive') + for kind in os.listdir(sample_archive): + shutil.copyfile(os.path.join(sample_archive, kind), + os.path.join(archive_dir, kind)) + + for kind in storage.ALL_FOUR: + os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)), + os.path.join(live_dir, '{0}.pem'.format(kind))) + + conf_path = os.path.join(self.config_dir, conf_dir, testfile) + with open(vector_path(testfile)) as src: + with open(conf_path, 'w') as dst: + dst.writelines( + line.replace('MAGICDIR', self.config_dir) for line in src) + + return conf_path diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 6f06c8306..cd02d835d 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -11,7 +11,7 @@ import mock import six from certbot import errors -from certbot.tests import test_util +import certbot.tests.util as test_util class RunScriptTest(unittest.TestCase): @@ -195,6 +195,7 @@ except NameError: import io file_type = io.TextIOWrapper + class UniqueLineageNameTest(unittest.TestCase): """Tests for certbot.util.unique_lineage_name.""" @@ -373,6 +374,11 @@ class EnforceDomainSanityTest(unittest.TestCase): self.assertRaises(errors.ConfigurationError, self._call, u"eichh\u00f6rnchen.example.com") + def test_punycode_ok(self): + # Punycode is now legal, so no longer an error; instead check + # that it's _not_ an error (at the initial sanity check stage) + self._call('this.is.xn--ls8h.tld') + class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" From feef1b411b30732701a1e791f5655ec7481e2ef3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 5 Dec 2016 17:00:04 -0800 Subject: [PATCH 280/331] Add pyasn1 back to le-auto (#3858) --- letsencrypt-auto-source/letsencrypt-auto | 12 ++++++++++++ .../pieces/letsencrypt-auto-requirements.txt | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 5c1efc9fe..3d2db3065 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -695,6 +695,18 @@ parsedatetime==2.1 \ pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 +pyasn1==0.1.9 \ + --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ + --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ + --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ + --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ + --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ + --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ + --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ + --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ + --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ + --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ + --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f pyOpenSSL==16.2.0 \ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index a00f0bbb9..5af713056 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -85,6 +85,18 @@ parsedatetime==2.1 \ pbr==1.8.1 \ --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 +pyasn1==0.1.9 \ + --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ + --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ + --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ + --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ + --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ + --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ + --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ + --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ + --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ + --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ + --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f pyOpenSSL==16.2.0 \ --hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \ --hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e From 3dbf5c9fcb809c03e8098d3ee493b2aafc3fc13b Mon Sep 17 00:00:00 2001 From: Timothy Guan-tin Chien Date: Tue, 6 Dec 2016 10:49:38 +0800 Subject: [PATCH 281/331] certbot-auto: Print link to doc on debugging pip install error [revision requested] (#3473) * certbot-auto: Print link to doc on debugging pip install error Also, update the doc to teach the user to workaround problem on a low memory system. * Correct formatting * grep the PIP_OUT and print useful info if the problem is about memory allocation * Fix logic on string to grep --- docs/install.rst | 17 +++++++++++++++ .../letsencrypt-auto.template | 21 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/docs/install.rst b/docs/install.rst index 910d23149..aa59e44ec 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -73,6 +73,23 @@ For full command line help, you can type:: ./certbot-auto --help all +Problems with Python virtual environment +---------------------------------------- + +On a low memory system such as VPS with only 256MB of RAM, the required dependencies of Certbot will failed to build. +This can be identified if the pip outputs contains something like ``internal compiler error: Killed (program cc1)``. +You can workaround this restriction by creating a temporary swapfile:: + + user@webserver:~$ sudo fallocate -l 1G /tmp/swapfile + user@webserver:~$ sudo chmod 600 /tmp/swapfile + user@webserver:~$ sudo mkswap /tmp/swapfile + user@webserver:~$ sudo swapon /tmp/swapfile + +Disable and remove the swapfile once the virtual enviroment is constructed:: + + user@webserver:~$ sudo swapoff /tmp/swapfile + user@webserver:~$ sudo rm /tmp/swapfile + Running with Docker ------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 619306979..68e6e9743 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -285,7 +285,28 @@ UNLIKELY_EOF # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages." if [ "$VERBOSE" != 1 ]; then + echo + echo "pip prints the following errors: " + echo "=====================================================" echo "$PIP_OUT" + echo "=====================================================" + echo + echo "Certbot has problem setting up the virtual environment." + + if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then + echo + echo "Based on your pip output, the problem can likely be fixed by " + echo "increasing the available memory." + else + echo + echo "We were not be able to guess the right solution from your pip " + echo "output." + fi + + echo + echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment" + echo "for possible solutions." + echo "You may also find some support resources at https://certbot.eff.org/support/ ." fi rm -rf "$VENV_PATH" exit 1 From f0a7bb0e33d2d864a83050f46ec1efdff1b47949 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Mon, 5 Dec 2016 19:17:04 -0800 Subject: [PATCH 282/331] Mark Nginx vhosts as ssl when any vhost is on ssl at that address (#3856) * Move parse_server to be a method of NginxParser * add super equal method to more correctly check addr equality in nginx should we support ipv6 in nginx in the future * add addr:normalized_tuple method * mark addresses listening sslishly due to another server block listening sslishly on that address * test turning on ssl globally * add docstring * lint and remove extra file --- certbot-nginx/certbot_nginx/obj.py | 9 +- certbot-nginx/certbot_nginx/parser.py | 152 ++++++++++++------ .../certbot_nginx/tests/configurator_test.py | 5 +- .../certbot_nginx/tests/parser_test.py | 34 +++- .../etc_nginx/sites-enabled/globalssl.com | 9 ++ certbot/plugins/common.py | 19 +-- 6 files changed, 158 insertions(+), 70 deletions(-) create mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 4a3ca865e..98bf86f5c 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -93,9 +93,16 @@ class Addr(common.Addr): def __repr__(self): return "Addr(" + self.__str__() + ")" + def super_eq(self, other): + """Check ip/port equality, with IPv6 support. + """ + # Nginx plugin currently doesn't support IPv6 but this will + # future-proof it + return super(Addr, self).__eq__(other) + def __eq__(self, other): if isinstance(other, self.__class__): - return (self.tup == other.tup and + return (self.super_eq(other) and self.ssl == other.ssl and self.default == other.default) return False diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 385635212..1a2c85c2c 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -82,21 +82,28 @@ class NginxParser(object): else: return path - def get_vhosts(self): - # pylint: disable=cell-var-from-loop - """Gets list of all 'virtual hosts' found in Nginx configuration. - Technically this is a misnomer because Nginx does not have virtual - hosts, it has 'server blocks'. - - :returns: List of :class:`~certbot_nginx.obj.VirtualHost` - objects found in configuration - :rtype: list - + def _build_addr_to_ssl(self): + """Builds a map from address to whether it listens on ssl in any server block """ - enabled = True # We only look at enabled vhosts for now - vhosts = [] - servers = {} + servers = self._get_raw_servers() + addr_to_ssl = {} + for filename in servers: + for server, _ in servers[filename]: + # Parse the server block to save addr info + parsed_server = _parse_server_raw(server) + for addr in parsed_server['addrs']: + addr_tuple = addr.normalized_tuple() + if addr_tuple not in addr_to_ssl: + addr_to_ssl[addr_tuple] = addr.ssl + addr_to_ssl[addr_tuple] = addr.ssl or addr_to_ssl[addr_tuple] + return addr_to_ssl + + def _get_raw_servers(self): + # pylint: disable=cell-var-from-loop + """Get a map of unparsed all server blocks + """ + servers = {} for filename in self.parsed: tree = self.parsed[filename] servers[filename] = [] @@ -110,12 +117,28 @@ class NginxParser(object): for i, (server, path) in enumerate(servers[filename]): new_server = self._get_included_directives(server) servers[filename][i] = (new_server, path) + return servers + def get_vhosts(self): + # pylint: disable=cell-var-from-loop + """Gets list of all 'virtual hosts' found in Nginx configuration. + Technically this is a misnomer because Nginx does not have virtual + hosts, it has 'server blocks'. + + :returns: List of :class:`~certbot_nginx.obj.VirtualHost` + objects found in configuration + :rtype: list + + """ + enabled = True # We only look at enabled vhosts for now + servers = self._get_raw_servers() + + vhosts = [] for filename in servers: for server, path in servers[filename]: # Parse the server block into a VirtualHost object - parsed_server = parse_server(server) + parsed_server = _parse_server_raw(server) vhost = obj.VirtualHost(filename, parsed_server['addrs'], parsed_server['ssl'], @@ -125,8 +148,20 @@ class NginxParser(object): path) vhosts.append(vhost) + self._update_vhosts_addrs_ssl(vhosts) + return vhosts + def _update_vhosts_addrs_ssl(self, vhosts): + """Update a list of raw parsed vhosts to include global address sslishness + """ + addr_to_ssl = self._build_addr_to_ssl() + for vhost in vhosts: + for addr in vhost.addrs: + addr.ssl = addr_to_ssl[addr.normalized_tuple()] + if addr.ssl: + vhost.ssl = True + def _get_included_directives(self, block): """Returns array with the "include" directives expanded out by concatenating the contents of the included file to the block. @@ -241,6 +276,17 @@ class NginxParser(object): except IOError: logger.error("Could not open file for writing: %s", filename) + def parse_server(self, server): + """Parses a list of server directives, accounting for global address sslishness. + + :param list server: list of directives in a server block + :rtype: dict + """ + addr_to_ssl = self._build_addr_to_ssl() + parsed_server = _parse_server_raw(server) + _apply_global_addr_ssl(addr_to_ssl, parsed_server) + return parsed_server + def has_ssl_on_directive(self, vhost): """Does vhost have ssl on for all ports? @@ -290,7 +336,7 @@ class NginxParser(object): # update vhost based on new directives new_server = self._get_included_directives(result) - parsed_server = parse_server(new_server) + parsed_server = self.parse_server(new_server) vhost.addrs = parsed_server['addrs'] vhost.ssl = parsed_server['ssl'] vhost.names = parsed_server['names'] @@ -434,41 +480,6 @@ def _get_servernames(names): names = re.sub(whitespace_re, ' ', names) return names.split(' ') - -def parse_server(server): - """Parses a list of server directives. - - :param list server: list of directives in a server block - :rtype: dict - - """ - parsed_server = {'addrs': set(), - 'ssl': False, - 'names': set()} - - apply_ssl_to_all_addrs = False - - for directive in server: - if not directive: - continue - if directive[0] == 'listen': - addr = obj.Addr.fromstring(directive[1]) - parsed_server['addrs'].add(addr) - if not parsed_server['ssl'] and addr.ssl: - parsed_server['ssl'] = True - elif directive[0] == 'server_name': - parsed_server['names'].update( - _get_servernames(directive[1])) - elif directive[0] == 'ssl' and directive[1] == 'on': - parsed_server['ssl'] = True - apply_ssl_to_all_addrs = True - - if apply_ssl_to_all_addrs: - for addr in parsed_server['addrs']: - addr.ssl = True - - return parsed_server - def _add_directives(block, directives, replace): """Adds or replaces directives in a config block. @@ -549,3 +560,44 @@ def _add_directive(block, directive, replace): 'tried to insert directive "{0}" but found ' 'conflicting "{1}".'.format(directive, block[location])) +def _apply_global_addr_ssl(addr_to_ssl, parsed_server): + """Apply global sslishness information to the parsed server block + """ + for addr in parsed_server['addrs']: + addr.ssl = addr_to_ssl[addr.normalized_tuple()] + if addr.ssl: + parsed_server['ssl'] = True + +def _parse_server_raw(server): + """Parses a list of server directives. + + :param list server: list of directives in a server block + :rtype: dict + + """ + parsed_server = {'addrs': set(), + 'ssl': False, + 'names': set()} + + apply_ssl_to_all_addrs = False + + for directive in server: + if not directive: + continue + if directive[0] == 'listen': + addr = obj.Addr.fromstring(directive[1]) + parsed_server['addrs'].add(addr) + if addr.ssl: + parsed_server['ssl'] = True + elif directive[0] == 'server_name': + parsed_server['names'].update( + _get_servernames(directive[1])) + elif directive[0] == 'ssl' and directive[1] == 'on': + parsed_server['ssl'] = True + apply_ssl_to_all_addrs = True + + if apply_ssl_to_all_addrs: + for addr in parsed_server['addrs']: + addr.ssl = True + + return parsed_server diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index f165ea23a..08a66fc98 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -40,7 +40,7 @@ class NginxConfiguratorTest(util.NginxTest): def test_prepare(self): self.assertEqual((1, 6, 2), self.config.version) - self.assertEqual(7, len(self.config.parser.parsed)) + self.assertEqual(8, len(self.config.parser.parsed)) # ensure we successfully parsed a file for ssl_options self.assertTrue(self.config.parser.loc["ssl_options"]) @@ -68,7 +68,8 @@ class NginxConfiguratorTest(util.NginxTest): names = self.config.get_all_names() self.assertEqual(names, set( ["155.225.50.69.nephoscale.net", "www.example.org", "another.alias", - "migration.com", "summer.com", "geese.com", "sslon.com"])) + "migration.com", "summer.com", "geese.com", "sslon.com", + "globalssl.com", "globalsslsetssl.com"])) def test_supported_enhancements(self): self.assertEqual(['redirect', 'staple-ocsp'], diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 54deffd7a..921cc3c5a 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -49,7 +49,8 @@ class NginxParserTest(util.NginxTest): 'sites-enabled/default', 'sites-enabled/example.com', 'sites-enabled/migration.com', - 'sites-enabled/sslon.com']]), + 'sites-enabled/sslon.com', + 'sites-enabled/globalssl.com']]), set(nparser.parsed.keys())) self.assertEqual([['server_name', 'somename alias another.alias']], nparser.parsed[nparser.abs_path('server.conf')]) @@ -73,7 +74,7 @@ class NginxParserTest(util.NginxTest): parsed = nparser._parse_files(nparser.abs_path( 'sites-enabled/example.com.test')) self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test')))) - self.assertEqual(4, len( + self.assertEqual(5, len( glob.glob(nparser.abs_path('sites-enabled/*.test')))) self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'], @@ -104,6 +105,16 @@ class NginxParserTest(util.NginxTest): lambda x, y, pts=paths: pts.append(y)) self.assertEqual(paths, result) + def test_get_vhosts_global_ssl(self): + nparser = parser.NginxParser(self.config_path, self.ssl_options) + vhosts = nparser.get_vhosts() + + vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'), + [obj.Addr('4.8.2.6', '57', True, False)], + True, True, set(['globalssl.com']), [], [0]) + + globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0] + self.assertEqual(vhost, globalssl_com) def test_get_vhosts(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) @@ -137,7 +148,7 @@ class NginxParserTest(util.NginxTest): '*.www.example.com']), [], [2, 1, 0]) - self.assertEqual(8, len(vhosts)) + self.assertEqual(10, len(vhosts)) example_com = [x for x in vhosts if 'example.com' in x.filep][0] self.assertEqual(vhost3, example_com) default = [x for x in vhosts if 'default' in x.filep][0] @@ -291,27 +302,34 @@ class NginxParserTest(util.NginxTest): COMMENT_BLOCK, ["\n", "e", " ", "f"]]) - def test_parse_server_ssl(self): - server = parser.parse_server([ + def test_parse_server_raw_ssl(self): + server = parser._parse_server_raw([ #pylint: disable=protected-access ['listen', '443'] ]) self.assertFalse(server['ssl']) - server = parser.parse_server([ + server = parser._parse_server_raw([ #pylint: disable=protected-access ['listen', '443 ssl'] ]) self.assertTrue(server['ssl']) - server = parser.parse_server([ + server = parser._parse_server_raw([ #pylint: disable=protected-access ['listen', '443'], ['ssl', 'off'] ]) self.assertFalse(server['ssl']) - server = parser.parse_server([ + server = parser._parse_server_raw([ #pylint: disable=protected-access ['listen', '443'], ['ssl', 'on'] ]) self.assertTrue(server['ssl']) + def test_parse_server_global_ssl_applied(self): + nparser = parser.NginxParser(self.config_path, self.ssl_options) + server = nparser.parse_server([ + ['listen', '443'] + ]) + self.assertTrue(server['ssl']) + def test_ssl_options_should_be_parsed_ssl_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) self.assertEqual(nginxparser.UnspacedList(nparser.loc["ssl_options"]), diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com new file mode 100644 index 000000000..969447d6e --- /dev/null +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/globalssl.com @@ -0,0 +1,9 @@ +server { + server_name globalssl.com; + listen 4.8.2.6:57; +} + +server { + server_name globalsslsetssl.com; + listen 4.8.2.6:57 ssl; +} diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index 007105c7b..46d4c5740 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -127,17 +127,18 @@ class Addr(object): return "%s:%s" % self.tup return self.tup[0] + def normalized_tuple(self): + """Normalized representation of addr/port tuple + """ + if self.ipv6: + return (self._normalize_ipv6(self.tup[0]), self.tup[1]) + return self.tup + def __eq__(self, other): if isinstance(other, self.__class__): - if self.ipv6: - # compare normalized to take different - # styles of representation into account - return (other.ipv6 and - self._normalize_ipv6(self.tup[0]) == - self._normalize_ipv6(other.tup[0]) and - self.tup[1] == other.tup[1]) - else: - return self.tup == other.tup + # compare normalized to take different + # styles of representation into account + return self.normalized_tuple() == other.normalized_tuple() return False From 184d67337892d044a78e55e72778d705a579c09d Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 6 Dec 2016 04:40:07 +0100 Subject: [PATCH 283/331] Busybox support (#3797) * Added support for shells without default variable support * Added support for BusyBox installs that do not have `command` but has `which` * Style fixes as suggested by reviewer * Renamed `WHERE_IS` to `EXISTS` as suggested by review * Removed expansion of `$LE_AUTO_SUDO` to `x` as the `-n` can check empty strings. * Added `EXISTS` to debian bootstrap as suggested in review --- .../letsencrypt-auto.template | 23 +++++++++++++++---- .../pieces/bootstrappers/deb_common.sh | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 68e6e9743..d7c497260 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -15,9 +15,13 @@ set -e # Work even if somebody does "sh thisscript.sh". # Note: you can set XDG_DATA_HOME or VENV_PATH before running this script, # if you want to change where the virtual environment will be installed -XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} +if [ -z "$XDG_DATA_HOME" ]; then + XDG_DATA_HOME="~/.local/share" +fi VENV_NAME="letsencrypt" -VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} +if [ -z "$VENV_PATH" ]; then + VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" +fi VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}" BASENAME=$(basename $0) @@ -80,6 +84,17 @@ if [ $BASENAME = "letsencrypt-auto" ]; then HELP=0 fi +# Support for busybox and others where there is no "command", +# but "which" instead +if command -v command > /dev/null 2>&1 ; then + export EXISTS="command -v" +elif which which > /dev/null 2>&1 ; then + export EXISTS="which" +else + echo "Cannot find command nor which... please install one!" + exit 1 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -127,7 +142,7 @@ if [ -n "${LE_AUTO_SUDO+x}" ]; then echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'." else if test "`id -u`" -ne "0" ; then - if command -v sudo 1>/dev/null 2>&1; then + if $EXISTS sudo 1>/dev/null 2>&1; then SUDO=sudo SUDO_ENV="CERTBOT_AUTO=$0" else @@ -157,7 +172,7 @@ ExperimentalBootstrap() { DeterminePythonVersion() { for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do # Break (while keeping the LE_PYTHON value) if found. - command -v "$LE_PYTHON" > /dev/null && break + $EXISTS "$LE_PYTHON" > /dev/null && break done if [ "$?" != "0" ]; then echo "Cannot find any Pythons; please install one!" diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index 0188cadc5..747ab8c8d 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -101,7 +101,7 @@ BootstrapDebCommon() { - if ! command -v virtualenv > /dev/null ; then + if ! $EXISTS virtualenv > /dev/null ; then echo Failed to install a working \"virtualenv\" command, exiting exit 1 fi From 59c602d9caf2fdba8ad09fc501b5a6bc6c3e5342 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 6 Dec 2016 20:39:16 -0800 Subject: [PATCH 284/331] Parallalelise nosetests from tox (#3836) * Parallalelise nosetests from tox * Parallelise even more things, break even more things * Now unbreak all the tests that aren't ready for ||ism * Try to pass tests! - Remove non-working hack in reporter_test - also be selective about ||ism in the cover environment * Try again * certbot-apache tests also work, given enough time * Nginx may need more time in Travis's cloud * Unbreak reporter_test under ||ism * More timeout * Working again? * This goes way faster * Another big win * Split a couple more large test suites * A last improvement * More ||ism! * ||ise lint too * Allow nosetests to figure out how many cores to use * simplify merge * Mark the new CLI tests as ||izable * Simplify reporter_test changes * Rationalise ||ism flags * Re-up coverage * Clean up reporter tests * Stop modifying testdata during tests * remove unused os --- acme/acme/crypto_util_test.py | 6 ++ acme/acme/standalone_test.py | 8 +++ .../tests/augeas_configurator_test.py | 2 + .../certbot_apache/tests/configurator_test.py | 3 + .../certbot_nginx/tests/configurator_test.py | 2 + .../certbot_nginx/tests/nginxparser_test.py | 56 ++++++++----------- certbot/tests/cli_test.py | 9 +++ certbot/tests/reporter_test.py | 10 ++-- certbot/tests/storage_test.py | 2 + tox.cover.sh | 5 +- tox.ini | 28 ++++------ 11 files changed, 76 insertions(+), 55 deletions(-) diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 913b50164..bd93ae0e1 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -18,6 +18,8 @@ from acme import test_util class SSLSocketAndProbeSNITest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket/probe_sni.""" + _multiprocess_can_split_ = True + def setUp(self): self.cert = test_util.load_comparable_cert('rsa2048_cert.pem') key = test_util.load_pyopenssl_private_key('rsa2048_key.pem') @@ -67,6 +69,8 @@ class SSLSocketAndProbeSNITest(unittest.TestCase): class PyOpenSSLCertOrReqSANTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_san.""" + _multiprocess_can_split_ = True + @classmethod def _call(cls, loader, name): # pylint: disable=protected-access @@ -131,6 +135,8 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase): class RandomSnTest(unittest.TestCase): """Test for random certificate serial numbers.""" + _multiprocess_can_split_ = True + def setUp(self): self.cert_count = 5 self.serial_num = [] diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 92a0b4272..58469d470 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -21,6 +21,8 @@ from acme import test_util class TLSServerTest(unittest.TestCase): """Tests for acme.standalone.TLSServer.""" + _multiprocess_can_split_ = True + def test_bind(self): # pylint: disable=no-self-use from acme.standalone import TLSServer server = TLSServer( @@ -31,6 +33,8 @@ class TLSServerTest(unittest.TestCase): class TLSSNI01ServerTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01Server.""" + _multiprocess_can_split_ = True + def setUp(self): self.certs = {b'localhost': ( test_util.load_pyopenssl_private_key('rsa2048_key.pem'), @@ -57,6 +61,8 @@ class TLSSNI01ServerTest(unittest.TestCase): class HTTP01ServerTest(unittest.TestCase): """Tests for acme.standalone.HTTP01Server.""" + _multiprocess_can_split_ = True + def setUp(self): self.account_key = jose.JWK.load( test_util.load_vector('rsa1024_key.pem')) @@ -109,6 +115,8 @@ class HTTP01ServerTest(unittest.TestCase): class TestSimpleTLSSNI01Server(unittest.TestCase): """Tests for acme.standalone.simple_tls_sni_01_server.""" + _multiprocess_can_split_ = True + def setUp(self): # mirror ../examples/standalone self.test_cwd = tempfile.mkdtemp() diff --git a/certbot-apache/certbot_apache/tests/augeas_configurator_test.py b/certbot-apache/certbot_apache/tests/augeas_configurator_test.py index c55f27ff0..66da017ec 100644 --- a/certbot-apache/certbot_apache/tests/augeas_configurator_test.py +++ b/certbot-apache/certbot_apache/tests/augeas_configurator_test.py @@ -13,6 +13,8 @@ from certbot_apache.tests import util class AugeasConfiguratorTest(util.ApacheTest): """Test for Augeas Configurator base class.""" + _multiprocess_can_split_ = True + def setUp(self): # pylint: disable=arguments-differ super(AugeasConfiguratorTest, self).setUp() diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 5f4685e96..2f1d01315 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -24,6 +24,8 @@ from certbot_apache.tests import util class MultipleVhostsTest(util.ApacheTest): """Test two standard well-configured HTTP vhosts.""" + _multiprocess_can_split_ = True + def setUp(self): # pylint: disable=arguments-differ super(MultipleVhostsTest, self).setUp() @@ -1241,6 +1243,7 @@ class MultipleVhostsTest(util.ApacheTest): class AugeasVhostsTest(util.ApacheTest): """Test vhosts with illegal names dependant on augeas version.""" # pylint: disable=protected-access + _multiprocess_can_split_ = True def setUp(self): # pylint: disable=arguments-differ td = "debian_apache_2_4/augeas_vhosts" diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 08a66fc98..fd442d88e 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -21,6 +21,8 @@ from certbot_nginx.tests import util class NginxConfiguratorTest(util.NginxTest): """Test a semi complex vhost configuration.""" + _multiprocess_can_split_ = True + def setUp(self): super(NginxConfiguratorTest, self).setUp() diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 5c8d6d215..e83b414cf 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -1,7 +1,7 @@ """Test for certbot_nginx.nginxparser.""" import copy import operator -import os +import tempfile import unittest from pyparsing import ParseException @@ -128,44 +128,34 @@ class TestRawNginxParser(unittest.TestCase): [['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 = load(handle) - try: - self.maxDiff = None - self.assertEqual(parsed[0], parsed_new[0]) - self.assertEqual(parsed[1:], parsed_new[1:]) - finally: - os.unlink(util.get_data_filename('nginx.new.conf')) + with tempfile.TemporaryFile() as f: + dump(parsed, f) + f.seek(0) + parsed_new = load(f) + self.assertEqual(parsed, parsed_new) def test_comments(self): with open(util.get_data_filename('minimalistic_comments.conf')) as handle: parsed = load(handle) - with open(util.get_data_filename('minimalistic_comments.new.conf'), 'w') as handle: - dump(parsed, handle) + with tempfile.TemporaryFile() as f: + dump(parsed, f) + f.seek(0) + parsed_new = load(f) - with open(util.get_data_filename('minimalistic_comments.new.conf')) as handle: - parsed_new = load(handle) - - try: - self.assertEqual(parsed, parsed_new) - - 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')) + self.assertEqual(parsed, parsed_new) + 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;']]], + ]) def test_issue_518(self): parsed = loads('if ($http_accept ~* "webp") { set $webp "true"; }') diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 0c9f73f6a..03c8c6fbd 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -20,6 +20,9 @@ def reset_set_by_cli(): class TestReadFile(unittest.TestCase): '''Test cli.read_file''' + + _multiprocess_can_split_ = True + def test_read_file(self): tmp_dir = tempfile.mkdtemp() rel_test_path = os.path.relpath(os.path.join(tmp_dir, 'foo')) @@ -38,6 +41,8 @@ class TestReadFile(unittest.TestCase): class ParseTest(unittest.TestCase): '''Test the cli args entrypoint''' + _multiprocess_can_split_ = True + @classmethod def setUpClass(cls): cls.plugins = disco.PluginsRegistry.find_all() @@ -256,6 +261,8 @@ class ParseTest(unittest.TestCase): class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" + _multiprocess_can_split_ = True + def setUp(self): # pylint: disable=protected-access self.default1 = cli._Default() @@ -275,6 +282,8 @@ class DefaultTest(unittest.TestCase): class SetByCliTest(unittest.TestCase): """Tests for certbot.set_by_cli and related functions.""" + _multiprocess_can_split_ = True + def setUp(self): reload_module(cli) diff --git a/certbot/tests/reporter_test.py b/certbot/tests/reporter_test.py index 02c7981b7..0b06cccd7 100644 --- a/certbot/tests/reporter_test.py +++ b/certbot/tests/reporter_test.py @@ -8,7 +8,6 @@ import six class ReporterTest(unittest.TestCase): """Tests for certbot.reporter.Reporter.""" - def setUp(self): from certbot import reporter self.reporter = reporter.Reporter(mock.MagicMock(quiet=False)) @@ -21,7 +20,7 @@ class ReporterTest(unittest.TestCase): def test_multiline_message(self): self.reporter.add_message("Line 1\nLine 2", self.reporter.LOW_PRIORITY) - self.reporter.atexit_print_messages() + self.reporter.print_messages() output = sys.stdout.getvalue() self.assertTrue("Line 1\n" in output) self.assertTrue("Line 2" in output) @@ -39,9 +38,12 @@ class ReporterTest(unittest.TestCase): self.reporter.print_messages() self.assertEqual(sys.stdout.getvalue(), "") - def test_atexit_print_messages(self): + @mock.patch('certbot.reporter.os.getpid') + def test_atexit_print_messages(self, mock_getpid): self._add_messages() - self.reporter.atexit_print_messages() + mock_getpid.return_value = 42 + with mock.patch('certbot.reporter.INITIAL_PID', 42): + self.reporter.atexit_print_messages() output = sys.stdout.getvalue() self.assertTrue("IMPORTANT NOTES:" in output) self.assertTrue("High" in output) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 17c4524e0..1b212449e 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -43,6 +43,8 @@ class BaseRenewableCertTest(unittest.TestCase): your test. Check :class:`.cli_test.DuplicateCertTest` for an example. """ + _multiprocess_can_split_ = True + def setUp(self): from certbot import storage self.tempdir = tempfile.mkdtemp() diff --git a/tox.cover.sh b/tox.cover.sh index 7243c4708..d138a98e5 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -36,8 +36,9 @@ cover () { # specific package, positional argument scopes tests only to # specific package directory; --cover-tests makes sure every tests # is run (c.f. #403) - nosetests -c /dev/null --with-cover --cover-tests --cover-package \ - "$1" --cover-min-percentage="$min" "$1" + nosetests -c /dev/null --with-cover --cover-tests --cover-package \ + "$1" --cover-min-percentage="$min" "$1" --processes=-1 \ + --process-timeout=100 } rm -f .coverage # --cover-erase is off, make sure stats are correct diff --git a/tox.ini b/tox.ini index a6a017efe..1d82092f6 100644 --- a/tox.ini +++ b/tox.ini @@ -14,15 +14,15 @@ envlist = modification,py{26,33,34,35},cover,lint # are detected, c.f. #1002 commands = pip install -e acme[dns,dev] - nosetests -v acme + nosetests -v acme --processes=-1 pip install -e .[dev] - nosetests -v certbot + nosetests -v certbot --processes=-1 --process-timeout=100 pip install -e certbot-apache - nosetests -v certbot_apache + nosetests -v certbot_apache --processes=-1 --process-timeout=80 pip install -e certbot-nginx - nosetests -v certbot_nginx + nosetests -v certbot_nginx --processes=-1 pip install -e letshelp-certbot - nosetests -v letshelp_certbot + nosetests -v letshelp_certbot --processes=-1 setenv = PYTHONPATH = {toxinidir} @@ -45,23 +45,23 @@ deps = [testenv:py33] commands = pip install -e acme[dns,dev] - nosetests -v acme + nosetests -v acme --processes=-1 pip install -e .[dev] - nosetests -v certbot + nosetests -v certbot --processes=-1 --process-timeout=100 [testenv:py34] commands = pip install -e acme[dns,dev] - nosetests -v acme + nosetests -v acme --processes=-1 pip install -e .[dev] - nosetests -v certbot + nosetests -v certbot --processes=-1 --process-timeout=100 [testenv:py35] commands = pip install -e acme[dns,dev] - nosetests -v acme + nosetests -v acme --processes=-1 pip install -e .[dev] - nosetests -v certbot + nosetests -v certbot --processes=-1 --process-timeout=100 [testenv:cover] basepython = python2.7 @@ -78,12 +78,8 @@ basepython = python2.7 commands = pip install -q -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot ./pep8.travis.sh - pylint --reports=n --rcfile=.pylintrc certbot pylint --reports=n --rcfile=acme/.pylintrc acme/acme - pylint --reports=n --rcfile=.pylintrc certbot-apache/certbot_apache - pylint --reports=n --rcfile=.pylintrc certbot-nginx/certbot_nginx - pylint --reports=n --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test - pylint --reports=n --rcfile=.pylintrc letshelp-certbot/letshelp_certbot + pylint -j 0 --reports=n --rcfile=.pylintrc certbot certbot-apache/certbot_apache certbot-nginx/certbot_nginx certbot-compatibility-test/certbot_compatibility_test letshelp-certbot/letshelp_certbot [testenv:apacheconftest] #basepython = python2.7 From cc86ff2a216ce6686cb4f826ae0892589e9f744d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 7 Dec 2016 16:02:13 -0800 Subject: [PATCH 285/331] Improve the "certbot certificates" output (#3846) * Begin making "certbot certificates" future safe * Handle the case where a renewal conf file has no "server" entry --- certbot/cert_manager.py | 14 ++++++++------ certbot/renewal.py | 8 ++------ certbot/storage.py | 12 +++++++++++- certbot/tests/cert_manager_test.py | 12 ++++++++++-- certbot/tests/storage_test.py | 13 +++++++++++++ certbot/util.py | 12 ++++++++++++ 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index a3237253e..f16d19201 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -39,20 +39,22 @@ def _report_human_readable(parsed_certs): certinfo = [] for cert in parsed_certs: now = pytz.UTC.fromutc(datetime.datetime.utcnow()) - if cert.target_expiry <= now: - expiration_text = "EXPIRED" + if cert.is_test_cert: + expiration_text = "INVALID: TEST CERT" + elif cert.target_expiry <= now: + expiration_text = "INVALID: EXPIRED" else: diff = cert.target_expiry - now if diff.days == 1: - expiration_text = "1 day" + expiration_text = "VALID: 1 day" elif diff.days < 1: - expiration_text = "under 1 day" + expiration_text = "VALID: {0} hour(s)".format(diff.seconds // 3600) else: - expiration_text = "{0} days".format(diff.days) + expiration_text = "VALID: {0} days".format(diff.days) valid_string = "{0} ({1})".format(cert.target_expiry, expiration_text) certinfo.append(" Certificate Name: {0}\n" " Domains: {1}\n" - " Valid Until: {2}\n" + " Expiry Date: {2}\n" " Certificate Path: {3}\n" " Private Key Path: {4}".format( cert.lineagename, diff --git a/certbot/renewal.py b/certbot/renewal.py index aa39c5fad..09a58ed1b 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -13,7 +13,6 @@ import OpenSSL from certbot import configuration from certbot import cli -from certbot import constants from certbot import crypto_util from certbot import errors @@ -209,9 +208,6 @@ def should_renew(config, lineage): def _avoid_invalidating_lineage(config, lineage, original_server): "Do not renew a valid cert with one from a staging server!" - def _is_staging(srv): - return srv == constants.STAGING_URI or "staging" in srv - # Some lineages may have begun with --staging, but then had production certs # added to them latest_cert = OpenSSL.crypto.load_certificate( @@ -220,8 +216,8 @@ def _avoid_invalidating_lineage(config, lineage, original_server): # we should test more methodically now_valid = "fake" not in repr(latest_cert.get_issuer()).lower() - if _is_staging(config.server): - if not _is_staging(original_server) or now_valid: + if util.is_staging(config.server): + if not util.is_staging(original_server) or now_valid: if not config.break_my_certs: names = ", ".join(lineage.names()) raise errors.Error( diff --git a/certbot/storage.py b/certbot/storage.py index 1fc13a5df..2536e59ca 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -172,7 +172,8 @@ def relevant_values(all_values): if _relevant(option) and cli.option_was_set(option, value)) -class RenewableCert(object): # pylint: disable=too-many-instance-attributes +class RenewableCert(object): + # pylint: disable=too-many-instance-attributes,too-many-public-methods """Renewable certificate. Represents a lineage of certificates that is under the management of @@ -281,6 +282,15 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes return os.path.join( self.cli_config.default_archive_dir, self.lineagename) + @property + def is_test_cert(self): + """Returns true if this is a test cert from a staging server.""" + server = self.configuration["renewalparams"].get("server", None) + if server: + return util.is_staging(server) + else: + return False + def _check_symlinks(self): """Raises an exception if a symlink doesn't exist""" for kind in ALL_FOUR: diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index c67ab5e50..68e095cd6 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -157,26 +157,34 @@ class CertificatesTest(BaseCertManagerTest): cert = mock.MagicMock(lineagename="nameone") cert.target_expiry = expiry cert.names.return_value = ["nameone", "nametwo"] + cert.is_test_cert = False parsed_certs = [cert] # pylint: disable=protected-access out = cert_manager._report_human_readable(parsed_certs) - self.assertTrue('EXPIRED' in out) + self.assertTrue("INVALID: EXPIRED" in out) cert.target_expiry += datetime.timedelta(hours=2) # pylint: disable=protected-access out = cert_manager._report_human_readable(parsed_certs) - self.assertTrue('under 1 day' in out) + self.assertTrue('1 hour(s)' in out) + self.assertTrue('VALID' in out and not 'INVALID' in out) cert.target_expiry += datetime.timedelta(days=1) # pylint: disable=protected-access out = cert_manager._report_human_readable(parsed_certs) self.assertTrue('1 day' in out) self.assertFalse('under' in out) + self.assertTrue('VALID' in out and not 'INVALID' in out) cert.target_expiry += datetime.timedelta(days=2) # pylint: disable=protected-access out = cert_manager._report_human_readable(parsed_certs) self.assertTrue('3 days' in out) + self.assertTrue('VALID' in out and not 'INVALID' in out) + + cert.is_test_cert = True + out = cert_manager._report_human_readable(parsed_certs) + self.assertTrue('INVALID: TEST CERT' in out) if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 1b212449e..ebe7d2243 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -705,6 +705,19 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(storage.add_time_interval(base_time, interval), excepted) + def test_is_test_cert(self): + self.test_rc.configuration["renewalparams"] = {} + rp = self.test_rc.configuration["renewalparams"] + self.assertEqual(self.test_rc.is_test_cert, False) + rp["server"] = "https://acme-staging.api.letsencrypt.org/directory" + self.assertEqual(self.test_rc.is_test_cert, True) + rp["server"] = "https://staging.someotherca.com/directory" + self.assertEqual(self.test_rc.is_test_cert, True) + rp["server"] = "https://acme-v01.api.letsencrypt.org/directory" + self.assertEqual(self.test_rc.is_test_cert, False) + rp["server"] = "https://acme-v02.api.letsencrypt.org/directory" + self.assertEqual(self.test_rc.is_test_cert, False) + def test_missing_cert(self): from certbot import storage self.assertRaises(errors.CertStorageError, diff --git a/certbot/util.py b/certbot/util.py index 220795237..1597c5bd8 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -17,6 +17,7 @@ import sys import configargparse +from certbot import constants from certbot import errors @@ -499,3 +500,14 @@ def get_strict_version(normalized): # strict version ending with "a" and a number designates a pre-release # pylint: disable=no-member return distutils.version.StrictVersion(normalized.replace(".dev", "a")) + + +def is_staging(srv): + """ + Determine whether a given ACME server is a known test / staging server. + + :param str srv: the URI for the ACME server + :returns: True iff srv is a known test / staging server + :rtype bool: + """ + return srv == constants.STAGING_URI or "staging" in srv From 0a7ca2f32e89a65b93bca0c0e540ff004633e059 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 8 Dec 2016 11:53:30 -0800 Subject: [PATCH 286/331] Implement the --cert-name flag to select a lineage by its name, and the rename verb. (#3785) * Rename and simplify main functions * pass certname to auth method * find cert by certname flag * Implement --cert-name command * don't ask to confirm new cert when we have domains and no existing certs with the lineage name * Refactor and add --new-cert-name flag * add interactivity to rename verb * allow noninteractive and more descriptive function names --- certbot/cert_manager.py | 109 +++++++++++++ certbot/cli.py | 23 ++- certbot/client.py | 8 +- certbot/main.py | 136 ++++++++++------- certbot/renewal.py | 10 +- certbot/storage.py | 33 +++- certbot/tests/cert_manager_test.py | 236 +++++++++++++++++++++++++++++ certbot/tests/cli_test.py | 1 + certbot/tests/client_test.py | 28 +++- certbot/tests/main_test.py | 130 +++++++++------- certbot/util.py | 1 - 11 files changed, 590 insertions(+), 125 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index f16d19201..0f6f4c730 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -1,14 +1,19 @@ """Tools for managing certificates.""" import datetime import logging +import os import pytz import traceback import zope.component from certbot import configuration +from certbot import errors from certbot import interfaces from certbot import renewal from certbot import storage +from certbot import util + +from certbot.display import util as display_util logger = logging.getLogger(__name__) @@ -30,6 +35,43 @@ def update_live_symlinks(config): configuration.RenewerConfiguration(renewer_config), update_symlinks=True) +def rename_lineage(config): + """Rename the specified lineage to the new name. + + :param config: Configuration. + :type config: :class:`certbot.interfaces.IConfig` + + """ + disp = zope.component.getUtility(interfaces.IDisplay) + renewer_config = configuration.RenewerConfiguration(config) + + certname = config.certname + if not certname: + filenames = renewal.renewal_conf_files(renewer_config) + choices = [storage.lineagename_for_filename(name) for name in filenames] + if not choices: + raise errors.Error("No existing certificates found.") + code, index = disp.menu("Which certificate would you like to rename?", + choices, ok_label="Select", flag="--cert-name") + if code != display_util.OK or not index in range(0, len(choices)): + raise errors.Error("User ended interaction.") + certname = choices[index] + + new_certname = config.new_certname + if not new_certname: + code, new_certname = disp.input("Enter the new name for certificate {0}" + .format(certname), flag="--updated-cert-name") + if code != display_util.OK or not new_certname: + raise errors.Error("User ended interaction.") + + lineage = lineage_for_certname(config, certname) + if not lineage: + raise errors.ConfigurationError("No existing certificate with name " + "{0} found.".format(certname)) + storage.rename_renewal_config(certname, new_certname, renewer_config) + disp.notification("Successfully renamed {0} to {1}." + .format(certname, new_certname), pause=False) + def _report_lines(msgs): """Format a results report for a category of single-line renewal outcomes""" return " " + "\n ".join(str(msg) for msg in msgs) @@ -106,3 +148,70 @@ def certificates(config): # Describe all the certs _describe_certs(parsed_certs, parse_failures) + +def _search_lineages(config, func, initial_rv): + """Iterate func over unbroken lineages, allowing custom return conditions. + + Allows flexible customization of return values, including multiple + return values and complex checks. + """ + cli_config = configuration.RenewerConfiguration(config) + configs_dir = cli_config.renewal_configs_dir + # Verify the directory is there + util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) + + rv = initial_rv + for renewal_file in renewal.renewal_conf_files(cli_config): + try: + candidate_lineage = storage.RenewableCert(renewal_file, cli_config) + except (errors.CertStorageError, IOError): + logger.debug("Renewal conf file %s is broken. Skipping.", renewal_file) + logger.debug("Traceback was:\n%s", traceback.format_exc()) + continue + rv = func(candidate_lineage, rv) + return rv + +def lineage_for_certname(config, certname): + """Find a lineage object with name certname.""" + def update_cert_for_name_match(candidate_lineage, rv): + """Return cert if it has name certname, else return rv + """ + matching_lineage_name_cert = rv + if candidate_lineage.lineagename == certname: + matching_lineage_name_cert = candidate_lineage + return matching_lineage_name_cert + return _search_lineages(config, update_cert_for_name_match, None) + +def domains_for_certname(config, certname): + """Find the domains in the cert with name certname.""" + def update_domains_for_name_match(candidate_lineage, rv): + """Return domains if certname matches, else return rv + """ + matching_domains = rv + if candidate_lineage.lineagename == certname: + matching_domains = candidate_lineage.names() + return matching_domains + return _search_lineages(config, update_domains_for_name_match, None) + +def find_duplicative_certs(config, domains): + """Find existing certs that duplicate the request.""" + def update_certs_for_domain_matches(candidate_lineage, rv): + """Return cert as identical_names_cert if it matches, + or subset_names_cert if it matches as subset + """ + # TODO: Handle these differently depending on whether they are + # expired or still valid? + identical_names_cert, subset_names_cert = rv + candidate_names = set(candidate_lineage.names()) + if candidate_names == set(domains): + identical_names_cert = candidate_lineage + elif candidate_names.issubset(set(domains)): + # This logic finds and returns the largest subset-names cert + # in the case where there are several available. + if subset_names_cert is None: + subset_names_cert = candidate_lineage + elif len(candidate_names) > len(subset_names_cert.names()): + subset_names_cert = candidate_lineage + return (identical_names_cert, subset_names_cert) + + return _search_lineages(config, update_certs_for_domain_matches, (None, None)) diff --git a/certbot/cli.py b/certbot/cli.py index d327240f4..356a03764 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -68,6 +68,7 @@ cert. Major SUBCOMMANDS are: rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation update_symlinks Update cert symlinks based on renewal config file + rename Update a certificate's name plugins Display information about installed plugins certificates Display information about certs configured with Certbot @@ -326,7 +327,7 @@ class HelpfulArgumentParser(object): "register": main.register, "renew": main.renew, "revoke": main.revoke, "rollback": main.rollback, "everything": main.run, "update_symlinks": main.update_symlinks, - "certificates": main.certificates} + "certificates": main.certificates, "rename": main.rename} # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) @@ -686,6 +687,19 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains " "as a parameter.") + helpful.add( + [None, "run", "certonly"], + "--cert-name", dest="certname", + metavar="CERTNAME", default=None, + help="Certificate name to apply. Only one certificate name can be used " + "per Certbot run. To see certificate names, run 'certbot certificates'." + "If there is no existing certificate with this name and " + "domains are requested, create a new certificate with this name.") + helpful.add( + "rename", + "--updated-cert-name", dest="new_certname", + metavar="NEW_CERTNAME", default=None, + help="New name for the certificate. Must be a valid filename.") helpful.add( [None, "testing", "renew", "certonly"], "--dry-run", action="store_true", dest="dry_run", @@ -738,6 +752,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "regardless of whether it is near expiry. (Often " "--keep-until-expiring is more appropriate). Also implies " "--expand.") + helpful.add( + "automation", "--renew-with-new-domains", + action="store_true", dest="renew_with_new_domains", help="If a " + "certificate already exists for the requested certificate name " + "but does not match the requested domains, renew it now, " + "regardless of whether it is near expiry.") helpful.add( ["automation", "renew", "certonly"], "--allow-subset-of-names", action="store_true", @@ -1015,7 +1035,6 @@ class _DomainsAction(argparse.Action): """Just wrap add_domains in argparseese.""" add_domains(namespace, domain) - def add_domains(args_or_config, domains): """Registers new domains to be used during the current client run. diff --git a/certbot/client.py b/certbot/client.py index 4d6de6375..d58f9457f 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -263,7 +263,7 @@ class Client(object): return (self.obtain_certificate_from_csr(domains, csr, authzr=authzr) + (key, csr)) - def obtain_and_enroll_certificate(self, domains): + def obtain_and_enroll_certificate(self, domains, certname): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified @@ -272,6 +272,7 @@ class Client(object): :param list domains: Domains to request. :param plugins: A PluginsFactory object. + :param str certname: Name of new cert :returns: A new :class:`certbot.storage.RenewableCert` instance referred to the enrolled cert lineage, False if the cert could not @@ -286,13 +287,14 @@ class Client(object): "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") + new_name = certname if certname else domains[0] if self.config.dry_run: logger.debug("Dry run: Skipping creating new lineage for %s", - domains[0]) + new_name) return None else: return storage.RenewableCert.new_lineage( - domains[0], OpenSSL.crypto.dump_certificate( + new_name, OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped), key.pem, crypto_util.dump_pyopenssl_chain(chain), configuration.RenewerConfiguration(self.config.namespace)) diff --git a/certbot/main.py b/certbot/main.py index 471dcd838..e3c271162 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -30,7 +30,6 @@ from certbot import interfaces from certbot import util from certbot import reporter from certbot import renewal -from certbot import storage from certbot.display import util as display_util, ops as display_ops from certbot.plugins import disco as plugins_disco @@ -42,6 +41,9 @@ _PERM_ERR_FMT = os.linesep.join(( "If running as non-root, set --config-dir, " "--logs-dir, and --work-dir to writeable paths.")) +USER_CANCELLED = ("User chose to cancel the operation and may " + "reinvoke the client.") + logger = logging.getLogger(__name__) @@ -68,17 +70,21 @@ def _report_successful_dry_run(config): reporter_util.HIGH_PRIORITY, on_crash=False) -def _auth_from_domains(le_client, config, domains, lineage=None): +def _auth_from_available(le_client, config, domains=None, certname=None, lineage=None): """Authenticate and enroll certificate. - :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal + This method finds the relevant lineage, figures out what to do with it, + then performs that action. Includes calls to hooks, various reports, + checks, and requests for user input. + + :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" """ # If lineage is specified, use that one instead of looking around for # a matching one. if lineage is None: # This will find a relevant matching lineage that exists - action, lineage = _treat_as_renewal(config, domains) + action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname) else: # Renewal, where we already know the specific lineage we're # interested in @@ -94,11 +100,11 @@ def _auth_from_domains(le_client, config, domains, lineage=None): try: if action == "renew": logger.info("Renewing an existing certificate") - renewal.renew_cert(config, domains, le_client, lineage) + renewal.renew_cert(config, le_client, lineage) elif action == "newcert": # TREAT AS NEW REQUEST logger.info("Obtaining a new certificate") - lineage = le_client.obtain_and_enroll_certificate(domains) + lineage = le_client.obtain_and_enroll_certificate(domains, certname) if lineage is False: raise errors.Error("Certificate could not be obtained") finally: @@ -115,7 +121,7 @@ def _handle_subset_cert_request(config, domains, cert): :param storage.RenewableCert cert: - :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal + :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" :rtype: tuple @@ -147,9 +153,7 @@ def _handle_subset_cert_request(config, domains, cert): br=os.linesep ), reporter_util.HIGH_PRIORITY) - raise errors.Error( - "User chose to cancel the operation and may " - "reinvoke the client.") + raise errors.Error(USER_CANCELLED) def _handle_identical_cert_request(config, lineage): @@ -157,7 +161,7 @@ def _handle_identical_cert_request(config, lineage): :param storage.RenewableCert lineage: - :returns: Tuple of (str action, cert_or_None) as per _treat_as_renewal + :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname action can be: "newcert" | "renew" | "reinstall" :rtype: tuple @@ -171,8 +175,8 @@ def _handle_identical_cert_request(config, lineage): # reinstalled without further prompting. return "reinstall", lineage question = ( - "You have an existing certificate that contains exactly the same " - "domains you requested and isn't close to expiry." + "You have an existing certificate that has exactly the same " + "domains or certificate name you requested and isn't close to expiry." "{br}(ref: {0}){br}{br}What would you like to do?" ).format(lineage.configfile.filename, br=os.linesep) @@ -198,8 +202,7 @@ def _handle_identical_cert_request(config, lineage): else: assert False, "This is impossible" - -def _treat_as_renewal(config, domains): +def _find_lineage_for_domains(config, domains): """Determine whether there are duplicated names and how to handle them (renew, reinstall, newcert, or raising an error to stop the client run if the user chooses to cancel the operation when @@ -219,7 +222,7 @@ def _treat_as_renewal(config, domains): if config.duplicate: return "newcert", None # TODO: Also address superset case - ident_names_cert, subset_names_cert = _find_duplicative_certs(config, domains) + ident_names_cert, subset_names_cert = cert_manager.find_duplicative_certs(config, domains) # XXX ^ schoen is not sure whether that correctly reads the systemwide # configuration file. if ident_names_cert is None and subset_names_cert is None: @@ -230,51 +233,66 @@ def _treat_as_renewal(config, domains): elif subset_names_cert is not None: return _handle_subset_cert_request(config, domains, subset_names_cert) +def _find_lineage_for_domains_and_certname(config, domains, certname): + """Find appropriate lineage based on given domains and/or certname. -def _find_duplicative_certs(config, domains): - """Find existing certs that duplicate the request.""" + :returns: Two-element tuple containing desired new-certificate behavior as + a string token ("reinstall", "renew", or "newcert"), plus either + a RenewableCert instance or None if renewal shouldn't occur. - identical_names_cert, subset_names_cert = None, None + :raises .Error: If the user would like to rerun the client again. - cli_config = configuration.RenewerConfiguration(config) - configs_dir = cli_config.renewal_configs_dir - # Verify the directory is there - util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) + """ + if not certname: + return _find_lineage_for_domains(config, domains) + else: + lineage = cert_manager.lineage_for_certname(config, certname) + if lineage: + if domains: + if set(cert_manager.domains_for_certname(config, certname)) != set(domains): + _ask_user_to_confirm_new_names(config, domains, certname, + lineage.names()) # raises if no + return "renew", lineage + # unnecessarily specified domains or no domains specified + return _handle_identical_cert_request(config, lineage) + else: + if domains: + return "newcert", None + else: + raise errors.ConfigurationError("No certificate with name {0} found. " + "Use -d to specify domains, or run certbot --certificates to see " + "possible certificate names.".format(certname)) - for renewal_file in renewal.renewal_conf_files(cli_config): - try: - candidate_lineage = storage.RenewableCert(renewal_file, cli_config) - except (errors.CertStorageError, IOError): - logger.warning("Renewal conf file %s is broken. Skipping.", renewal_file) - logger.debug("Traceback was:\n%s", traceback.format_exc()) - continue - # TODO: Handle these differently depending on whether they are - # expired or still valid? - candidate_names = set(candidate_lineage.names()) - if candidate_names == set(domains): - identical_names_cert = candidate_lineage - elif candidate_names.issubset(set(domains)): - # This logic finds and returns the largest subset-names cert - # in the case where there are several available. - if subset_names_cert is None: - subset_names_cert = candidate_lineage - elif len(candidate_names) > len(subset_names_cert.names()): - subset_names_cert = candidate_lineage +def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): + """Ask user to confirm update cert certname to contain new_domains. + """ + if config.renew_with_new_domains: + return + msg = ("Confirm that you intend to update certificate {0} " + "to include domains {1}. Note that it previously " + "contained domains {2}.".format( + certname, + new_domains, + old_domains)) + obj = zope.component.getUtility(interfaces.IDisplay) + if not obj.yesno(msg, "Update cert", "Cancel", default=True): + raise errors.ConfigurationError("Specified mismatched cert name and domains.") - return identical_names_cert, subset_names_cert - - -def _find_domains(config, installer): +def _find_domains_or_certname(config, installer): + """Retrieve domains and certname from config or user input. + """ + domains = None if config.domains: domains = config.domains - else: + elif not config.certname: domains = display_ops.choose_names(installer) - if not domains: + if not domains and not config.certname: raise errors.Error("Please specify --domains, or --installer that " - "will help in domain names autodiscovery") + "will help in domain names autodiscovery, or " + "--cert-name for an existing certificate name.") - return domains + return domains, config.certname def _report_new_cert(config, cert_path, fullchain_path): @@ -429,7 +447,7 @@ def install(config, plugins): except errors.PluginSelectionError as e: return e.message - domains = _find_domains(config, installer) + domains, _ = _find_domains_or_certname(config, installer) le_client = _init_le_client(config, authenticator=None, installer=installer) assert config.cert_path is not None # required=True in the subparser le_client.deploy_certificate( @@ -485,6 +503,14 @@ def update_symlinks(config, unused_plugins): """ cert_manager.update_live_symlinks(config) +def rename(config, unused_plugins): + """Rename a certificate + + Use the information in the config file to rename an existing + lineage. + """ + cert_manager.rename_lineage(config) + def certificates(config, unused_plugins): """Display information about certs configured with Certbot """ @@ -521,12 +547,12 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals except errors.PluginSelectionError as e: return e.message - domains = _find_domains(config, installer) + domains, certname = _find_domains_or_certname(config, installer) # TODO: Handle errors from _init_le_client? le_client = _init_le_client(config, authenticator, installer) - action, lineage = _auth_from_domains(le_client, config, domains) + action, lineage = _auth_from_available(le_client, config, domains, certname) le_client.deploy_certificate( domains, lineage.privkey, lineage.cert, @@ -576,8 +602,8 @@ def obtain_cert(config, plugins, lineage=None): # SHOWTIME: Possibly obtain/renew a cert, and set action to renew | newcert | reinstall if config.csr is None: # the common case - domains = _find_domains(config, installer) - action, _ = _auth_from_domains(le_client, config, domains, lineage) + domains, certname = _find_domains_or_certname(config, installer) + action, _ = _auth_from_available(le_client, config, domains, certname, lineage) else: assert lineage is None, "Did not expect a CSR with a RenewableCert" _csr_obtain_cert(config, le_client) diff --git a/certbot/renewal.py b/certbot/renewal.py index 09a58ed1b..064df8bd2 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -226,12 +226,12 @@ def _avoid_invalidating_lineage(config, lineage, original_server): "unless you use the --break-my-certs flag!".format(names)) -def renew_cert(config, domains, le_client, lineage): +def renew_cert(config, le_client, lineage): "Renew a certificate lineage." renewal_params = lineage.configuration["renewalparams"] original_server = renewal_params.get("server", cli.flag_default("server")) _avoid_invalidating_lineage(config, lineage, original_server) - new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) + new_certr, new_chain, new_key, _ = le_client.obtain_certificate(lineage.names()) if config.dry_run: logger.debug("Dry run: skipping updating lineage at %s", os.path.dirname(lineage.cert)) @@ -245,7 +245,7 @@ def renew_cert(config, domains, le_client, lineage): lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, renewal_conf) lineage.update_all_links_to(lineage.latest_common_version()) - hooks.renew_hook(config, domains, lineage.live_dir) + hooks.renew_hook(config, lineage.names(), lineage.live_dir) def report(msgs, category): @@ -300,12 +300,12 @@ def renew_all_lineages(config): """Examine each lineage; renew if due and report results""" # This is trivially False if config.domains is empty - if any(domain not in config.webroot_map for domain in config.domains): + if any(domain not in config.webroot_map for domain in config.domains) or config.certname: # If more plugins start using cli.add_domains, # we may want to only log a warning here raise errors.Error("Currently, the renew verb is only capable of " "renewing all installed certificates that are due " - "to be renewed; individual domains cannot be " + "to be renewed; individual domains or lineages cannot be " "specified with this action. If you would like to " "renew specific certificates, use the certonly " "command. The renew verb may provide other options " diff --git a/certbot/storage.py b/certbot/storage.py index 2536e59ca..61ab69ff7 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -96,6 +96,25 @@ def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_d config.write(outfile=f) return config +def rename_renewal_config(prev_name, new_name, cli_config): + """Renames cli_config.certname's config to cli_config.new_certname. + + :param .RenewerConfiguration cli_config: parsed command line + arguments + """ + prev_filename = os.path.join( + cli_config.renewal_configs_dir, prev_name) + ".conf" + new_filename = os.path.join( + cli_config.renewal_configs_dir, new_name) + ".conf" + if os.path.exists(new_filename): + raise errors.ConfigurationError("The new certificate name " + "is already in use.") + try: + os.rename(prev_filename, new_filename) + except OSError: + raise errors.ConfigurationError("Please specify a valid filename " + "for the new certificate name.") + def update_configuration(lineagename, archive_dir, target, cli_config): """Modifies lineagename's config to contain the specified values. @@ -171,6 +190,14 @@ def relevant_values(all_values): for option, value in six.iteritems(all_values) if _relevant(option) and cli.option_was_set(option, value)) +def lineagename_for_filename(config_filename): + """Returns the lineagename for a configuration filename. + """ + if not config_filename.endswith(".conf"): + raise errors.CertStorageError( + "renewal config file name must end in .conf") + return os.path.basename(config_filename[:-len(".conf")]) + class RenewableCert(object): # pylint: disable=too-many-instance-attributes,too-many-public-methods @@ -220,11 +247,7 @@ class RenewableCert(object): """ self.cli_config = cli_config - if not config_filename.endswith(".conf"): - raise errors.CertStorageError( - "renewal config file name must end in .conf") - self.lineagename = os.path.basename( - config_filename[:-len(".conf")]) + self.lineagename = lineagename_for_filename(config_filename) # self.configuration should be used to read parameters that # may have been chosen based on default values from the diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 68e095cd6..f3569dc00 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -8,8 +8,15 @@ import unittest import configobj import mock +from certbot import configuration +from certbot import errors + +from certbot.display import util as display_util from certbot.storage import ALL_FOUR +from certbot.tests import storage_test +from certbot.tests import util as test_util + class BaseCertManagerTest(unittest.TestCase): """Base class for setting up Cert Manager tests. """ @@ -63,6 +70,7 @@ class BaseCertManagerTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tempdir) + class UpdateLiveSymlinksTest(BaseCertManagerTest): """Tests for certbot.cert_manager.update_live_symlinks """ @@ -96,6 +104,7 @@ class UpdateLiveSymlinksTest(BaseCertManagerTest): self.assertEqual(os.readlink(self.configs[domain][kind]), archive_paths[domain][kind]) + class CertificatesTest(BaseCertManagerTest): """Tests for certbot.cert_manager.certificates """ @@ -186,5 +195,232 @@ class CertificatesTest(BaseCertManagerTest): out = cert_manager._report_human_readable(parsed_certs) self.assertTrue('INVALID: TEST CERT' in out) + +class SearchLineagesTest(unittest.TestCase): + """Tests for certbot.cert_manager._search_lineages.""" + + @mock.patch('certbot.configuration.RenewerConfiguration') + @mock.patch('certbot.util.make_or_verify_dir') + @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.RenewableCert') + def test_cert_storage_error(self, mock_renewable_cert, mock_renewal_conf_files, + mock_make_or_verify_dir, mock_renewer_config): + mock_renewal_conf_files.return_value = ["badfile"] + mock_renewable_cert.side_effect = errors.CertStorageError + from certbot import cert_manager + # pylint: disable=protected-access + self.assertEqual(cert_manager._search_lineages(None, lambda x: x, "check"), "check") + self.assertTrue(mock_make_or_verify_dir.called) + self.assertTrue(mock_renewer_config) + + +class LineageForCertnameTest(unittest.TestCase): + """Tests for certbot.cert_manager.lineage_for_certname""" + + @mock.patch('certbot.configuration.RenewerConfiguration') + @mock.patch('certbot.util.make_or_verify_dir') + @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.RenewableCert') + def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files, + mock_make_or_verify_dir, mock_renewer_config): + mock_renewal_conf_files.return_value = ["somefile.conf"] + mock_match = mock.Mock(lineagename="example.com") + mock_renewable_cert.return_value = mock_match + from certbot import cert_manager + self.assertEqual(cert_manager.lineage_for_certname(None, "example.com"), mock_match) + self.assertTrue(mock_make_or_verify_dir.called) + self.assertTrue(mock_renewer_config) + + @mock.patch('certbot.configuration.RenewerConfiguration') + @mock.patch('certbot.util.make_or_verify_dir') + @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.RenewableCert') + def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files, + mock_make_or_verify_dir, mock_renewer_config): + mock_renewal_conf_files.return_value = ["somefile.conf"] + mock_match = mock.Mock(lineagename="other.com") + mock_renewable_cert.return_value = mock_match + from certbot import cert_manager + self.assertEqual(cert_manager.lineage_for_certname(None, "example.com"), None) + self.assertTrue(mock_make_or_verify_dir.called) + self.assertTrue(mock_renewer_config) + + +class DomainsForCertnameTest(unittest.TestCase): + """Tests for certbot.cert_manager.domains_for_certname""" + + @mock.patch('certbot.configuration.RenewerConfiguration') + @mock.patch('certbot.util.make_or_verify_dir') + @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.RenewableCert') + def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files, + mock_make_or_verify_dir, mock_renewer_config): + mock_renewal_conf_files.return_value = ["somefile.conf"] + mock_match = mock.Mock(lineagename="example.com") + domains = ["example.com", "example.org"] + mock_match.names.return_value = domains + mock_renewable_cert.return_value = mock_match + from certbot import cert_manager + self.assertEqual(cert_manager.domains_for_certname(None, "example.com"), domains) + self.assertTrue(mock_make_or_verify_dir.called) + self.assertTrue(mock_renewer_config) + + @mock.patch('certbot.configuration.RenewerConfiguration') + @mock.patch('certbot.util.make_or_verify_dir') + @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.RenewableCert') + def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files, + mock_make_or_verify_dir, mock_renewer_config): + mock_renewal_conf_files.return_value = ["somefile.conf"] + mock_match = mock.Mock(lineagename="example.com") + domains = ["example.com", "example.org"] + mock_match.names.return_value = domains + mock_renewable_cert.return_value = mock_match + from certbot import cert_manager + self.assertEqual(cert_manager.domains_for_certname(None, "other.com"), None) + self.assertTrue(mock_make_or_verify_dir.called) + self.assertTrue(mock_renewer_config) + + +class RenameLineageTest(BaseCertManagerTest): + """Tests for certbot.cert_manager.rename_lineage""" + + def setUp(self): + super(RenameLineageTest, self).setUp() + self.mock_config = configuration.RenewerConfiguration( + namespace=mock.MagicMock( + config_dir=self.tempdir, + work_dir=self.tempdir, + logs_dir=self.tempdir, + certname="example.org", + new_certname="after", + ) + ) + + def _call(self, *args, **kwargs): + from certbot import cert_manager + return cert_manager.rename_lineage(*args, **kwargs) + + @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.main.zope.component.getUtility') + def test_no_certname(self, mock_get_utility, mock_renewal_conf_files): + mock_config = mock.Mock(certname=None, new_certname="two") + + # if not choices + mock_renewal_conf_files.return_value = [] + self.assertRaises(errors.Error, self._call, mock_config) + + mock_renewal_conf_files.return_value = ["one.conf"] + util_mock = mock.Mock() + util_mock.menu.return_value = (display_util.CANCEL, 0) + mock_get_utility.return_value = util_mock + self.assertRaises(errors.Error, self._call, mock_config) + + util_mock.menu.return_value = (display_util.OK, -1) + self.assertRaises(errors.Error, self._call, mock_config) + + @mock.patch('certbot.main.zope.component.getUtility') + def test_no_new_certname(self, mock_get_utility): + mock_config = mock.Mock(certname="one", new_certname=None) + + util_mock = mock.Mock() + util_mock.input.return_value = (display_util.CANCEL, "name") + mock_get_utility.return_value = util_mock + self.assertRaises(errors.Error, self._call, mock_config) + + util_mock = mock.Mock() + util_mock.input.return_value = (display_util.OK, None) + mock_get_utility.return_value = util_mock + self.assertRaises(errors.Error, self._call, mock_config) + + @mock.patch('certbot.main.zope.component.getUtility') + @mock.patch('certbot.cert_manager.lineage_for_certname') + def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility): + mock_config = mock.Mock(certname="one", new_certname="two") + mock_lineage_for_certname.return_value = None + self.assertRaises(errors.ConfigurationError, + self._call, mock_config) + + @mock.patch('certbot.main.zope.component.getUtility') + @mock.patch("certbot.storage.RenewableCert._check_symlinks") + def test_rename_cert(self, mock_check, unused_get_utility): + mock_check.return_value = True + mock_config = self.mock_config + self._call(mock_config) + from certbot import cert_manager + updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname) + self.assertTrue(updated_lineage is not None) + self.assertEqual(updated_lineage.lineagename, mock_config.new_certname) + + @mock.patch('certbot.main.zope.component.getUtility') + @mock.patch("certbot.storage.RenewableCert._check_symlinks") + def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility): + mock_check.return_value = True + mock_config = self.mock_config + mock_config.certname = None + util_mock = mock.Mock() + util_mock.menu.return_value = (display_util.OK, 0) + mock_get_utility.return_value = util_mock + self._call(mock_config) + from certbot import cert_manager + updated_lineage = cert_manager.lineage_for_certname(mock_config, mock_config.new_certname) + self.assertTrue(updated_lineage is not None) + self.assertEqual(updated_lineage.lineagename, mock_config.new_certname) + + @mock.patch('certbot.main.zope.component.getUtility') + @mock.patch("certbot.storage.RenewableCert._check_symlinks") + def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility): + mock_check.return_value = True + mock_config = self.mock_config + + # for example, don't rename to existing certname + mock_config.new_certname = "example.org" + self.assertRaises(errors.ConfigurationError, self._call, mock_config) + + mock_config.new_certname = "one/two" + self.assertRaises(errors.ConfigurationError, self._call, mock_config) + + +class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): + """Test to avoid duplicate lineages.""" + + def setUp(self): + super(DuplicativeCertsTest, self).setUp() + self.config.write() + self._write_out_ex_kinds() + + def tearDown(self): + shutil.rmtree(self.tempdir) + + @mock.patch('certbot.util.make_or_verify_dir') + def test_find_duplicative_names(self, unused_makedir): + from certbot.cert_manager import find_duplicative_certs + test_cert = test_util.load_vector('cert-san.pem') + with open(self.test_rc.cert, 'wb') as f: + f.write(test_cert) + + # No overlap at all + result = find_duplicative_certs( + self.cli_config, ['wow.net', 'hooray.org']) + self.assertEqual(result, (None, None)) + + # Totally identical + result = find_duplicative_certs( + self.cli_config, ['example.com', 'www.example.com']) + self.assertTrue(result[0].configfile.filename.endswith('example.org.conf')) + self.assertEqual(result[1], None) + + # Superset + result = find_duplicative_certs( + self.cli_config, ['example.com', 'www.example.com', 'something.new']) + self.assertEqual(result[0], None) + self.assertTrue(result[1].configfile.filename.endswith('example.org.conf')) + + # Partial overlap doesn't count + result = find_duplicative_certs( + self.cli_config, ['example.com', 'something.new']) + self.assertEqual(result, (None, None)) + + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 03c8c6fbd..72aea50ea 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -125,6 +125,7 @@ class ParseTest(unittest.TestCase): self.assertTrue("--key-path" not in out) out = self._help_output(['-h']) + self.assertTrue(cli.usage_strings(self.plugins)[0] in out) def test_parse_domains(self): diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index b9f5cf15b..bf091a478 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -106,9 +106,14 @@ class RegisterTest(unittest.TestCase): class ClientTestCommon(unittest.TestCase): """Common base class for certbot.client.Client tests.""" def setUp(self): + self.config = mock.MagicMock( + no_verify_ssl=False, + config_dir="/etc/letsencrypt", + work_dir="/var/lib/letsencrypt", + allow_subset_of_names=False) + # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) - self.config = mock.MagicMock(no_verify_ssl=False) from certbot.client import Client with mock.patch("certbot.client.acme_client.Client") as acme: @@ -221,6 +226,27 @@ class ClientTest(ClientTestCommon): mock.sentinel.key, domains, self.config.csr_dir) self._check_obtain_certificate() + @mock.patch('certbot.client.Client.obtain_certificate') + @mock.patch('certbot.storage.RenewableCert.new_lineage') + @mock.patch('OpenSSL.crypto.dump_certificate') + def test_obtain_and_enroll_certificate(self, mock_dump_certificate, + mock_storage, mock_obtain_certificate): + domains = ["example.com", "www.example.com"] + mock_obtain_certificate.return_value = (mock.MagicMock(), + mock.MagicMock(), mock.MagicMock(), None) + + self.client.config.dry_run = False + self.assertTrue(self.client.obtain_and_enroll_certificate(domains, "example_cert")) + + self.assertTrue(self.client.obtain_and_enroll_certificate(domains, None)) + + self.client.config.dry_run = True + + self.assertFalse(self.client.obtain_and_enroll_certificate(domains, None)) + + self.assertTrue(mock_storage.call_count == 2) + self.assertTrue(mock_dump_certificate.call_count == 2) + @mock.patch("certbot.cli.helpful_parser") def test_save_certificate(self, mock_parser): # pylint: disable=too-many-locals diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 68e5ef00a..5e1ce1ab5 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -30,7 +30,6 @@ from certbot import util from certbot.plugins import disco from certbot.plugins import manual -from certbot.tests import storage_test import certbot.tests.util as test_util CERT_PATH = test_util.vector_path('cert.pem') @@ -56,7 +55,7 @@ class RunTest(unittest.TestCase): def setUp(self): self.domain = 'example.org' self.patches = [ - mock.patch('certbot.main._auth_from_domains'), + mock.patch('certbot.main._auth_from_available'), mock.patch('certbot.main.display_ops.success_installation'), mock.patch('certbot.main.display_ops.success_renewal'), mock.patch('certbot.main._init_le_client'), @@ -118,7 +117,7 @@ class ObtainCertTest(unittest.TestCase): return mock_init() # returns the client - @mock.patch('certbot.main._auth_from_domains') + @mock.patch('certbot.main._auth_from_available') def test_no_reinstall_text_pause(self, mock_auth): mock_notification = self.mock_get_utility().notification mock_notification.side_effect = self._assert_no_pause @@ -129,6 +128,71 @@ class ObtainCertTest(unittest.TestCase): # pylint: disable=unused-argument self.assertFalse(pause) + @mock.patch('certbot.cert_manager.lineage_for_certname') + @mock.patch('certbot.cert_manager.domains_for_certname') + @mock.patch('certbot.renewal.renew_cert') + @mock.patch('certbot.main._report_new_cert') + def test_find_lineage_for_domains_and_certname(self, mock_report_cert, + mock_renew_cert, mock_domains, mock_lineage): + domains = ['example.com', 'test.org'] + mock_domains.return_value = domains + mock_lineage.names.return_value = domains + self._call(('certonly --webroot -d example.com -d test.org ' + '--cert-name example.com').split()) + self.assertTrue(mock_lineage.call_count == 1) + self.assertTrue(mock_domains.call_count == 1) + self.assertTrue(mock_renew_cert.call_count == 1) + self.assertTrue(mock_report_cert.call_count == 1) + + # user confirms updating lineage with new domains + self._call(('certonly --webroot -d example.com -d test.com ' + '--cert-name example.com').split()) + self.assertTrue(mock_lineage.call_count == 2) + self.assertTrue(mock_domains.call_count == 2) + self.assertTrue(mock_renew_cert.call_count == 2) + self.assertTrue(mock_report_cert.call_count == 2) + + # error in _ask_user_to_confirm_new_names + util_mock = mock.Mock() + util_mock.yesno.return_value = False + self.mock_get_utility.return_value = util_mock + self.assertRaises(errors.ConfigurationError, self._call, + ('certonly --webroot -d example.com -d test.com --cert-name example.com').split()) + + @mock.patch('certbot.cert_manager.lineage_for_certname') + @mock.patch('certbot.main._report_new_cert') + def test_find_lineage_for_domains_new_certname(self, mock_report_cert, + mock_lineage): + mock_lineage.return_value = None + + # no lineage with this name but we specified domains so create a new cert + self._call(('certonly --webroot -d example.com -d test.com ' + '--cert-name example.com').split()) + self.assertTrue(mock_lineage.call_count == 1) + self.assertTrue(mock_report_cert.call_count == 1) + + # no lineage with this name and we didn't give domains + self.assertRaises(errors.ConfigurationError, self._call, + ('certonly --webroot --cert-name example.com').split()) + +class FindDomainsOrCertnameTest(unittest.TestCase): + """Tests for certbot.main._find_domains_or_certname.""" + + @mock.patch('certbot.display.ops.choose_names') + def test_display_ops(self, mock_choose_names): + mock_config = mock.Mock(domains=None, certname=None) + mock_choose_names.return_value = "domainname" + # pylint: disable=protected-access + self.assertEqual(main._find_domains_or_certname(mock_config, None), + ("domainname", None)) + + @mock.patch('certbot.display.ops.choose_names') + def test_no_results(self, mock_choose_names): + mock_config = mock.Mock(domains=None, certname=None) + mock_choose_names.return_value = [] + # pylint: disable=protected-access + self.assertRaises(errors.Error, main._find_domains_or_certname, mock_config, None) + class RevokeTest(unittest.TestCase): """Tests for certbot.main.revoke.""" @@ -333,47 +397,6 @@ class DetermineAccountTest(unittest.TestCase): self.assertEqual('other email', self.config.email) -class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): - """Test to avoid duplicate lineages.""" - - def setUp(self): - super(DuplicativeCertsTest, self).setUp() - self.config.write() - self._write_out_ex_kinds() - - def tearDown(self): - shutil.rmtree(self.tempdir) - - @mock.patch('certbot.util.make_or_verify_dir') - def test_find_duplicative_names(self, unused_makedir): - from certbot.main import _find_duplicative_certs - test_cert = test_util.load_vector('cert-san.pem') - with open(self.test_rc.cert, 'wb') as f: - f.write(test_cert) - - # No overlap at all - result = _find_duplicative_certs( - self.cli_config, ['wow.net', 'hooray.org']) - self.assertEqual(result, (None, None)) - - # Totally identical - result = _find_duplicative_certs( - self.cli_config, ['example.com', 'www.example.com']) - self.assertTrue(result[0].configfile.filename.endswith('example.org.conf')) - self.assertEqual(result[1], None) - - # Superset - result = _find_duplicative_certs( - self.cli_config, ['example.com', 'www.example.com', 'something.new']) - self.assertEqual(result[0], None) - self.assertTrue(result[1].configfile.filename.endswith('example.org.conf')) - - # Partial overlap doesn't count - result = _find_duplicative_certs( - self.cli_config, ['example.com', 'something.new']) - self.assertEqual(result, (None, None)) - - class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods """Tests for different commands.""" @@ -445,7 +468,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods self._cli_missing_flag(args, "specify a plugin") args.extend(['--standalone', '-d', 'eg.is']) self._cli_missing_flag(args, "register before running") - with mock.patch('certbot.main._auth_from_domains'): + with mock.patch('certbot.main._auth_from_available'): with mock.patch('certbot.main.client.acme_from_config_key'): args.extend(['--email', 'io@io.is']) self._cli_missing_flag(args, "--agree-tos") @@ -453,14 +476,14 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods @mock.patch('certbot.main.client.acme_client.Client') @mock.patch('certbot.main._determine_account') @mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate') - @mock.patch('certbot.main._auth_from_domains') - def test_user_agent(self, afd, _obt, det, _client): + @mock.patch('certbot.main._auth_from_available') + def test_user_agent(self, afa, _obt, det, _client): # Normally the client is totally mocked out, but here we need more # arguments to automate it... args = ["--standalone", "certonly", "-m", "none@none.com", "-d", "example.com", '--agree-tos'] + self.standard_args det.return_value = mock.MagicMock(), None - afd.return_value = "newcert", mock.MagicMock() + afa.return_value = "newcert", mock.MagicMock() with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: self._call_no_clientmock(args) @@ -512,8 +535,8 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") with mock.patch("certbot.main._init_le_client") as mock_init: - with mock.patch("certbot.main._auth_from_domains") as mock_afd: - mock_afd.return_value = (mock.MagicMock(), mock.MagicMock()) + with mock.patch("certbot.main._auth_from_available") as mock_afa: + mock_afa.return_value = (mock.MagicMock(), mock.MagicMock()) self._call(["certonly", "--manual", "-d", "foo.bar"]) unused_config, auth, unused_installer = mock_init.call_args[0] self.assertTrue(isinstance(auth, manual.Authenticator)) @@ -664,7 +687,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods 'certonly -d example.org --csr {0}'.format(CSR).split()) def _certonly_new_request_common(self, mock_client, args=None): - with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: + with mock.patch('certbot.main._find_lineage_for_domains_and_certname') as mock_renewal: mock_renewal.return_value = ("newcert", None) with mock.patch('certbot.main._init_le_client') as mock_init: mock_init.return_value = mock_client @@ -718,6 +741,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path) mock_lineage.should_autorenew.return_value = due_for_renewal mock_lineage.has_pending_deployment.return_value = False + mock_lineage.names.return_value = ['isnot.org'] mock_certr = mock.MagicMock() mock_key = mock.MagicMock(pem='pem_key') mock_client = mock.MagicMock() @@ -725,7 +749,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_client.obtain_certificate.return_value = (mock_certr, 'chain', mock_key, 'csr') try: - with mock.patch('certbot.main._find_duplicative_certs') as mock_fdc: + with mock.patch('certbot.cert_manager.find_duplicative_certs') as mock_fdc: mock_fdc.return_value = (mock_lineage, None) with mock.patch('certbot.main._init_le_client') as mock_init: mock_init.return_value = mock_client @@ -945,7 +969,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods should_renew=False, error_expected=True) @mock.patch('certbot.main.zope.component.getUtility') - @mock.patch('certbot.main._treat_as_renewal') + @mock.patch('certbot.main._find_lineage_for_domains_and_certname') @mock.patch('certbot.main._init_le_client') def test_certonly_reinstall(self, mock_init, mock_renewal, mock_get_utility): mock_renewal.return_value = ('reinstall', mock.MagicMock()) diff --git a/certbot/util.py b/certbot/util.py index 1597c5bd8..7d49a66a3 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -425,7 +425,6 @@ def enforce_le_validity(domain): label, domain)) return domain - def enforce_domain_sanity(domain): """Method which validates domain value and errors out if the requirements are not met. From 4a4977a54db7ddfa6b5a39c5803d3ef8dfbe7a39 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 8 Dec 2016 18:27:23 -0800 Subject: [PATCH 287/331] Make renew command respect the --cert-name flag (#3880) * Renew command respects --cert-name flag * Error out early if requested cert doesn't exist --- certbot/main.py | 2 +- certbot/renewal.py | 27 ++++++++++++++++++++------- certbot/tests/main_test.py | 10 ++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index e3c271162..395790faa 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -632,7 +632,7 @@ def obtain_cert(config, plugins, lineage=None): def renew(config, unused_plugins): """Renew previously-obtained certificates.""" try: - renewal.renew_all_lineages(config) + renewal.handle_renewal_request(config) finally: hooks.post_hook(config, final=True) diff --git a/certbot/renewal.py b/certbot/renewal.py index 064df8bd2..a057a63a9 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -38,6 +38,12 @@ def renewal_conf_files(config): """Return /path/to/*.conf in the renewal conf directory""" return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) +def renewal_file_for_certname(config, certname): + """Return /path/to/certname.conf in the renewal conf directory""" + path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname)) + if not os.path.exists(path): + raise errors.CertStorageError("No certificate found with name {0}.".format(certname)) + return path def _reconstitute(config, full_path): """Try to instantiate a RenewableCert, updating config with relevant items. @@ -296,26 +302,33 @@ def _renew_describe_results(config, renew_successes, renew_failures, print("\n".join(out)) -def renew_all_lineages(config): +def handle_renewal_request(config): """Examine each lineage; renew if due and report results""" # This is trivially False if config.domains is empty - if any(domain not in config.webroot_map for domain in config.domains) or config.certname: + if any(domain not in config.webroot_map for domain in config.domains): # If more plugins start using cli.add_domains, # we may want to only log a warning here - raise errors.Error("Currently, the renew verb is only capable of " + raise errors.Error("Currently, the renew verb is capable of either " "renewing all installed certificates that are due " - "to be renewed; individual domains or lineages cannot be " - "specified with this action. If you would like to " - "renew specific certificates, use the certonly " + "to be renewed or renewing a single certificate specified " + "by its name. If you would like to renew specific " + "certificates by their domains, use the certonly " "command. The renew verb may provide other options " "for selecting certificates to renew in the future.") + renewer_config = configuration.RenewerConfiguration(config) + + if config.certname: + conf_files = [renewal_file_for_certname(renewer_config, config.certname)] + else: + conf_files = renewal_conf_files(renewer_config) + renew_successes = [] renew_failures = [] renew_skipped = [] parse_failures = [] - for renewal_file in renewal_conf_files(renewer_config): + for renewal_file in conf_files: disp = zope.component.getUtility(interfaces.IDisplay) disp.notification("Processing " + renewal_file, pause=False) lineage_config = copy.deepcopy(config) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 5e1ce1ab5..a0d6cc418 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -872,6 +872,16 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True) + def test_renew_with_certname(self): + test_util.make_lineage(self, 'sample-renewal.conf') + self._test_renewal_common(True, [], should_renew=True, + args=['renew', '--dry-run', '--cert-name', 'sample-renewal']) + + def test_renew_with_bad_certname(self): + self._test_renewal_common(True, [], should_renew=False, + args=['renew', '--dry-run', '--cert-name', 'sample-renewal'], + error_expected=True) + def _make_dummy_renewal_config(self): renewer_configs_dir = os.path.join(self.config_dir, 'renewal') os.makedirs(renewer_configs_dir) From 9bdb3d67bcb264dd2b2fff1258f561cd0138c219 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 8 Dec 2016 20:29:59 -0800 Subject: [PATCH 288/331] make our linter happy (#3881) --- certbot-nginx/certbot_nginx/configurator.py | 4 +++- certbot-nginx/certbot_nginx/tests/configurator_test.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 0c6e1598c..f3acb5560 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -710,14 +710,16 @@ class NginxConfigurator(common.Plugin): """ save_files = set(self.parser.parsed.keys()) - try: + try: # TODO: make a common base for Apache and Nginx plugins # Create Checkpoint if temporary: self.reverter.add_to_temp_checkpoint( save_files, self.save_notes) + # how many comments does it take else: self.reverter.add_to_checkpoint(save_files, self.save_notes) + # to confuse a linter? except errors.ReverterError as err: raise errors.PluginError(str(err)) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index fd442d88e..6886127d7 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -345,15 +345,15 @@ class NginxConfiguratorTest(util.NginxTest): mock_popen.side_effect = OSError("Can't find program") self.assertRaises(errors.MisconfigurationError, self.config.restart) - @mock.patch("certbot.util.run_script") - def test_config_test(self, _): - self.config.config_test() - @mock.patch("certbot.util.run_script") def test_config_test_bad_process(self, mock_run_script): mock_run_script.side_effect = errors.SubprocessError self.assertRaises(errors.MisconfigurationError, self.config.config_test) + @mock.patch("certbot.util.run_script") + def test_config_test(self, _): + self.config.config_test() + @mock.patch("certbot.reverter.Reverter.recovery_routine") def test_recovery_routine_throws_error_from_reverter(self, mock_recovery_routine): mock_recovery_routine.side_effect = errors.ReverterError("foo") From 22e0f5779abe811e0f44c0a8d79b49b387e3dfdc Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 9 Dec 2016 14:56:14 -0800 Subject: [PATCH 289/331] Fix --debug (#3877) * Fix the --debug flag - Currently exceptions are often caught and burried in log files, even if this flag is provided! * Explain the insanity * Make things slightly nicer --- certbot/main.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 395790faa..2baab9670 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -702,10 +702,8 @@ def _handle_exception(exc_type, exc_value, trace, config): to the user. sys.exit is always called with a nonzero status. """ - logger.debug( - "Exiting abnormally:%s%s", - os.linesep, - "".join(traceback.format_exception(exc_type, exc_value, trace))) + tb_str = "".join(traceback.format_exception(exc_type, exc_value, trace)) + logger.debug("Exiting abnormally:%s%s", os.linesep, tb_str) if issubclass(exc_type, Exception) and (config is None or not config.debug): if config is None: @@ -715,8 +713,9 @@ def _handle_exception(exc_type, exc_value, trace, config): traceback.print_exception( exc_type, exc_value, trace, file=logfd) except: # pylint: disable=bare-except - sys.exit("".join( - traceback.format_exception(exc_type, exc_value, trace))) + sys.exit(tb_str) + if "--debug" in sys.argv: + sys.exit(tb_str) if issubclass(exc_type, errors.Error): sys.exit(exc_value) @@ -741,8 +740,7 @@ def _handle_exception(exc_type, exc_value, trace, config): msg += "logfiles in {0} for more details.".format(config.logs_dir) sys.exit(msg) else: - sys.exit("".join( - traceback.format_exception(exc_type, exc_value, trace))) + sys.exit(tb_str) def make_or_verify_core_dir(directory, mode, uid, strict): From e9f040e209815801d50b162db935cbebee180a38 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 10 Dec 2016 21:19:20 +0200 Subject: [PATCH 290/331] Make default renewal file permissions more strict (#3891) --- certbot/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/util.py b/certbot/util.py index 7d49a66a3..cbcfa3314 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -181,7 +181,7 @@ def unique_file(path, chmod=0o777, mode="w"): count=0, chmod=chmod, mode=mode) -def unique_lineage_name(path, filename, chmod=0o777, mode="w"): +def unique_lineage_name(path, filename, chmod=0o644, mode="w"): """Safely finds a unique file using lineage convention. :param str path: directory path From e6f24db6241bb189b2496569f6c7d43d9e5fcf08 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 11 Dec 2016 11:18:32 +0100 Subject: [PATCH 291/331] Sort the names by domain (then subdomain) before showing them (#3892) * Sort the names by domain (then subdomain) before showing them * Sort the names in display + tests --- certbot/display/ops.py | 15 +++++++++++- certbot/tests/display/ops_test.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index ee7e750f6..b9cd1e38c 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -130,6 +130,16 @@ def get_valid_domains(domains): continue return valid_domains +def _sort_names(FQDNs): + """Sort FQDNs by SLD (and if many, by their subdomains) + + :param list FQDNs: list of domain names + + :returns: Sorted list of domain names + :rtype: list + """ + return sorted(FQDNs, key=lambda fqdn: fqdn.split('.')[::-1][1:]) + def _filter_names(names): """Determine which names the user would like to select from a list. @@ -142,9 +152,12 @@ def _filter_names(names): :rtype: tuple """ + #Sort by domain first, and then by subdomain + sorted_names = _sort_names(names) + code, names = z_util(interfaces.IDisplay).checklist( "Which names would you like to activate HTTPS for?", - tags=names, cli_flag="--domains") + tags=sorted_names, cli_flag="--domains") return code, [str(s) for s in names] diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index bcef6088b..1b535bf3a 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -223,6 +223,45 @@ class ChooseNamesTest(unittest.TestCase): self.assertTrue( "configuration files" in mock_util().input.call_args[0][0]) + def test_sort_names_trivial(self): + from certbot.display.ops import _sort_names + + #sort an empty list + self.assertEqual(_sort_names([]), []) + + #sort simple domains + some_domains = ["ex.com", "zx.com", "ax.com"] + self.assertEqual(_sort_names(some_domains), ["ax.com", "ex.com", "zx.com"]) + + #Sort subdomains of a single domain + domain = ".ex.com" + unsorted_short = ["e", "a", "z", "y"] + unsorted_long = [us + domain for us in unsorted_short] + + sorted_short = sorted(unsorted_short) + sorted_long = [us + domain for us in sorted_short] + + self.assertEqual(_sort_names(unsorted_long), sorted_long) + + def test_sort_names_many(self): + from certbot.display.ops import _sort_names + + unsorted_domains = [".cx.com", ".bx.com", ".ax.com", ".dx.com"] + unsorted_short = ["www", "bnother.long.subdomain", "a", "a.long.subdomain", "z", "b"] + #Of course sorted doesn't work here ;-) + sorted_short = ["a", "b", "a.long.subdomain", "bnother.long.subdomain", "www", "z"] + + to_sort = [] + for short in unsorted_short: + for domain in unsorted_domains: + to_sort.append(short+domain) + sortd = [] + for domain in sorted(unsorted_domains): + for short in sorted_short: + sortd.append(short+domain) + self.assertEqual(_sort_names(to_sort), sortd) + + @mock.patch("certbot.display.ops.z_util") def test_filter_names_valid_return(self, mock_util): self.mock_install.get_all_names.return_value = set(["example.com"]) From 2d7f6d7d9290d48b60fd2c823a748f8a7dceb095 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 12 Dec 2016 17:20:52 -0800 Subject: [PATCH 292/331] Ensure apt-cache is always running in English if we're going to grep its output (#3900) --- letsencrypt-auto-source/letsencrypt-auto | 59 +++++++++++++++---- .../pieces/bootstrappers/deb_common.sh | 13 ++-- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 3d2db3065..d62a642ea 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -15,9 +15,13 @@ set -e # Work even if somebody does "sh thisscript.sh". # Note: you can set XDG_DATA_HOME or VENV_PATH before running this script, # if you want to change where the virtual environment will be installed -XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} +if [ -z "$XDG_DATA_HOME" ]; then + XDG_DATA_HOME="~/.local/share" +fi VENV_NAME="letsencrypt" -VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} +if [ -z "$VENV_PATH" ]; then + VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" +fi VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="0.10.0.dev0" BASENAME=$(basename $0) @@ -80,6 +84,17 @@ if [ $BASENAME = "letsencrypt-auto" ]; then HELP=0 fi +# Support for busybox and others where there is no "command", +# but "which" instead +if command -v command > /dev/null 2>&1 ; then + export EXISTS="command -v" +elif which which > /dev/null 2>&1 ; then + export EXISTS="which" +else + echo "Cannot find command nor which... please install one!" + exit 1 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -127,7 +142,7 @@ if [ -n "${LE_AUTO_SUDO+x}" ]; then echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'." else if test "`id -u`" -ne "0" ; then - if command -v sudo 1>/dev/null 2>&1; then + if $EXISTS sudo 1>/dev/null 2>&1; then SUDO=sudo SUDO_ENV="CERTBOT_AUTO=$0" else @@ -157,7 +172,7 @@ ExperimentalBootstrap() { DeterminePythonVersion() { for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do # Break (while keeping the LE_PYTHON value) if found. - command -v "$LE_PYTHON" > /dev/null && break + $EXISTS "$LE_PYTHON" > /dev/null && break done if [ "$?" != "0" ]; then echo "Cannot find any Pythons; please install one!" @@ -198,19 +213,22 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then - virtualenv="virtualenv" + # virtual env is known to apt and is installable + if apt-cache show virtualenv > /dev/null 2>&1 ; then + if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then + virtualenv="virtualenv" + fi fi if apt-cache show python-virtualenv > /dev/null 2>&1; then - virtualenv="$virtualenv python-virtualenv" + virtualenv="$virtualenv python-virtualenv" fi augeas_pkg="libaugeas0 augeas-lenses" - AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` if [ "$ASSUME_YES" = 1 ]; then - YES_FLAG="-y" + YES_FLAG="-y" fi AddBackportRepo() { @@ -276,7 +294,7 @@ BootstrapDebCommon() { - if ! command -v virtualenv > /dev/null ; then + if ! $EXISTS virtualenv > /dev/null ; then echo Failed to install a working \"virtualenv\" command, exiting exit 1 fi @@ -960,7 +978,28 @@ UNLIKELY_EOF # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages." if [ "$VERBOSE" != 1 ]; then + echo + echo "pip prints the following errors: " + echo "=====================================================" echo "$PIP_OUT" + echo "=====================================================" + echo + echo "Certbot has problem setting up the virtual environment." + + if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then + echo + echo "Based on your pip output, the problem can likely be fixed by " + echo "increasing the available memory." + else + echo + echo "We were not be able to guess the right solution from your pip " + echo "output." + fi + + echo + echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment" + echo "for possible solutions." + echo "You may also find some support resources at https://certbot.eff.org/support/ ." fi rm -rf "$VENV_PATH" exit 1 diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index 747ab8c8d..27919b67b 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -23,19 +23,22 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then - virtualenv="virtualenv" + # virtual env is known to apt and is installable + if apt-cache show virtualenv > /dev/null 2>&1 ; then + if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then + virtualenv="virtualenv" + fi fi if apt-cache show python-virtualenv > /dev/null 2>&1; then - virtualenv="$virtualenv python-virtualenv" + virtualenv="$virtualenv python-virtualenv" fi augeas_pkg="libaugeas0 augeas-lenses" - AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` if [ "$ASSUME_YES" = 1 ]; then - YES_FLAG="-y" + YES_FLAG="-y" fi AddBackportRepo() { From dc81c291b40a1676b5f56bd76e3169514afe98a4 Mon Sep 17 00:00:00 2001 From: Maarten Date: Tue, 13 Dec 2016 22:13:55 +0100 Subject: [PATCH 293/331] Change link of haproxy plugin to new version (#3904) Greenhost has rewritten their HAProxy plugin and it's hosted on a different location. The original URL also points to this new location: https://code.greenhost.net/open/letsencrypt-haproxy --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 7c1fac003..7764408bf 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -193,7 +193,7 @@ postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plu =========== ==== ==== =============================================================== .. _plesk: https://github.com/plesk/letsencrypt-plesk -.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy +.. _haproxy: https://github.com/greenhost/certbot-haproxy .. _s3front: https://github.com/dlapiduz/letsencrypt-s3front .. _gandi: https://github.com/Gandi/letsencrypt-gandi .. _icecast: https://github.com/e00E/lets-encrypt-icecast From 0464ba2c4b6afed56ecb72286b3cf01a54889baa Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 13 Dec 2016 14:19:47 -0800 Subject: [PATCH 294/331] Implement our fancy new --help output (#3883) * Start reorganising -h output * Fix the --debug flag - Currently exceptions are often caught and burried in log files, even if this flag is provided! * Explain the insanity * Parallalelise nosetests from tox (#3836) * Parallalelise nosetests from tox * Parallelise even more things, break even more things * Now unbreak all the tests that aren't ready for ||ism * Try to pass tests! - Remove non-working hack in reporter_test - also be selective about ||ism in the cover environment * Try again * certbot-apache tests also work, given enough time * Nginx may need more time in Travis's cloud * Unbreak reporter_test under ||ism * More timeout * Working again? * This goes way faster * Another big win * Split a couple more large test suites * A last improvement * More ||ism! * ||ise lint too * Allow nosetests to figure out how many cores to use * simplify merge * Mark the new CLI tests as ||izable * Simplify reporter_test changes * Rationalise ||ism flags * Re-up coverage * Clean up reporter tests * Stop modifying testdata during tests * remove unused os * Improve the "certbot certificates" output (#3846) * Begin making "certbot certificates" future safe * Handle the case where a renewal conf file has no "server" entry * Improvements, tweaks * Capitalise on things * Print the command summary for -h and -h all, but not otherwise Also, update nginx not installed CLI hint * Add a "certificates" help section * Clean up usage string construction * Greatly improve "certbot -h TOPIC" - subcommands now get their own usage headings if they want them - added "certbot -h commands" * A few more cli formatting tests * Auto-populate the verb subgroups from the docs * Show the new help output * Lint, tweak * More lint, and cleanup * Infinite lint * Add rename to command summary; sort "-h commands" output * Use fancy string formatting * More space * Implement --help manage Also, implement a general mechanism for documenting subcommands within topics * Remove one comma * Only create weird parser structures if -h is provided :) * Update sample cli out * Lint * Revert cli-help.txt to previous release version * Grammar & style --- certbot/cli.py | 305 +++++++++++++++++++++++++------------- certbot/main.py | 1 + certbot/tests/cli_test.py | 10 +- 3 files changed, 213 insertions(+), 103 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 356a03764..259953f5e 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -51,51 +51,55 @@ cli_command = LEAUTO if fragment in sys.argv[0] else "certbot" # to replace as much of it as we can... # This is the stub to include in help generated by argparse - SHORT_USAGE = """ - {0} [SUBCOMMAND] [options] [-d domain] [-d domain] ... + {0} [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ... Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing the -cert. Major SUBCOMMANDS are: +cert. """.format(cli_command) - (default) run Obtain & install a cert in your current webserver - certonly Obtain cert, but do not install it (aka "auth") - install Install a previously obtained cert in a server - renew Renew previously obtained certs that are near expiry - revoke Revoke a previously obtained certificate - register Perform tasks related to registering with the CA - rollback Rollback server configuration changes made during install - config_changes Show changes made to server config during installation - update_symlinks Update cert symlinks based on renewal config file - rename Update a certificate's name - plugins Display information about installed plugins - certificates Display information about certs configured with Certbot +# This section is used for --help and --help all ; it needs information +# about installed plugins to be fully formatted +COMMAND_OVERVIEW = """The most common SUBCOMMANDS and flags are: -""".format(cli_command) - -# This is the short help for certbot --help, where we disable argparse -# altogether -USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert: +obtain, install, and renew certificates: + (default) run Obtain & install a cert in your current webserver + certonly Obtain or renew a cert, but do not install it + renew Renew all previously obtained certs that are near expiry + -d DOMAINS Comma-separated list of domains to obtain a cert for %s --standalone Run a standalone webserver for authentication %s --webroot Place files in a server's webroot folder for authentication - --script User provided shell scripts for authentication + --manual Obtain certs interactively, or using shell script hoooks -OR use different plugins to obtain (authenticate) the cert and then install it: + -n Run non-interactively + --test-cert Obtain a test cert from a staging server + --dry-run Test "renew" or "certonly" without saving any certs to disk - --authenticator standalone --installer apache +manage certificates: + certificates Display information about certs you have from Certbot + revoke Revoke a certificate (supply --cert-path) + rename Rename a certificate +manage your account with Let's Encrypt: + register Create a Let's Encrypt ACME account + --agree-tos Agree to the ACME server's Subscriber Agreement + -m EMAIL Email address for important account notifications +""" + +# This is the short help for certbot --help, where we disable argparse +# altogether +HELP_USAGE = """ More detailed help: - -h, --help [topic] print this message, or detailed help on a topic; - the available topics are: + -h, --help [TOPIC] print this message, or detailed help on a topic; + the available TOPICS are: - all, automation, paths, security, testing, or any of the subcommands or - plugins (certonly, renew, install, register, nginx, apache, standalone, - webroot, script, etc.) + all, automation, commands, paths, security, testing, or any of the + subcommands or plugins (certonly, renew, install, register, nginx, + apache, standalone, webroot, script, etc.) """ @@ -141,19 +145,6 @@ def report_config_interaction(modified, modifiers): VAR_MODIFIERS.setdefault(var, set()).update(modifiers) -def usage_strings(plugins): - """Make usage strings late so that plugins can be initialised late""" - if "nginx" in plugins: - nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" - else: - nginx_doc = "(nginx support is experimental, buggy, and not installed by default)" - if "apache" in plugins: - apache_doc = "--apache Use the Apache plugin for authentication & installation" - else: - apache_doc = "(the apache plugin is not installed)" - return USAGE % (apache_doc, nginx_doc), SHORT_USAGE - - def possible_deprecation_warning(config): "A deprecation warning for users with the old, not-self-upgrading letsencrypt-auto." if cli_command != LEAUTO: @@ -309,6 +300,82 @@ class HelpfulArgumentGroup(object): """Add a new command line argument to the argument group.""" self._parser.add(self._topic, *args, **kwargs) +# The attributes here are: +# short: a string that will be displayed by "certbot -h commands" +# opts: a string that heads the section of flags with which this command is documented, +# both for "cerbot -h SUBCOMMAND" and "certbot -h all" +# usage: an optional string that overrides the header of "certbot -h SUBCOMMAND" +VERB_HELP = [ + ("run (default)", { + "short": "Obtain/renew a certificate, and install it", + "opts": "Options for obtaining & installing certs", + "usage": SHORT_USAGE.replace("[SUBCOMMAND]", ""), + "realname": "run" + }), + ("certonly", { + "short": "Obtain or renew a certificate, but do not install it", + "opts": "Options for modifying how a cert is obtained", + "usage": ("\n\n certbot certonly [options] [-d DOMAIN] [-d DOMAIN] ...\n\n" + "This command obtains a TLS/SSL certificate without installing it anywhere.") + }), + ("renew", { + "short": "Renew all certificates (or one specifed with --cert-name)", + "opts": ("The 'renew' subcommand will attempt to renew all" + " certificates (or more precisely, certificate lineages) you have" + " previously obtained if they are close to expiry, and print a" + " summary of the results. By default, 'renew' will reuse the options" + " used to create obtain or most recently successfully renew each" + " certificate lineage. You can try it with `--dry-run` first. For" + " more fine-grained control, you can renew individual lineages with" + " the `certonly` subcommand. Hooks are available to run commands" + " before and after renewal; see" + " https://certbot.eff.org/docs/using.html#renewal for more" + " information on these."), + "usage": "\n\n certbot renew [--cert-name NAME] [options]\n\n" + }), + ("certificates", { + "short": "List all certificates managed by Certbot", + "opts": "List all certificates managed by Certbot" + }), + ("revoke", { + "short": "Revoke a certificate specified with --cert-path", + "opts": "Options for revocation of certs" + }), + ("rename", { + "short": "Change a certificate's name (for management purposes)", + "opts": "Options changing certificate names" + }), + ("register", { + "short": "Register for account with Let's Encrypt / other ACME server", + "opts": "Options for account registration & modification" + }), + ("install", { + "short": "Install an arbitrary cert in a server", + "opts": "Options for modifying how a cert is deployed" + }), + ("config_changes", { + "short": "Show changes that Certbot has made to server configurations", + "opts": "Options for controlling which changes are displayed" + }), + ("rollback", { + "short": "Roll back server conf changes made during cert installation", + "opts": "Options for rolling back server configuration changes" + }), + ("plugins", { + "short": "List plugins that are installed and available on your system", + "opts": 'Options for for the "plugins" subcommand' + }), + ("update_symlinks", { + "short": "Recreate symlinks in your /live/ directory", + "opts": ("Recreates cert and key symlinks in {0}, if you changed them by hand " + "or edited a renewal configuration file".format( + os.path.join(flag_default("config_dir"), "live"))) + }), + +] +# VERB_HELP is a list in order to preserve order, but a dict is sometimes useful +VERB_HELP_MAP = dict(VERB_HELP) + class HelpfulArgumentParser(object): """Argparse Wrapper. @@ -319,6 +386,7 @@ class HelpfulArgumentParser(object): """ + def __init__(self, args, plugins, detect_defaults=False): from certbot import main self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert, @@ -331,22 +399,12 @@ class HelpfulArgumentParser(object): # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) + HELP_TOPICS += self.COMMANDS_TOPICS + ["manage"] plugin_names = list(plugins) self.help_topics = HELP_TOPICS + plugin_names + [None] - usage, short_usage = usage_strings(plugins) - self.parser = configargparse.ArgParser( - prog="certbot", - usage=short_usage, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - args_for_setting_config_path=["-c", "--config"], - default_config_files=flag_default("config_files")) - - # This is the only way to turn off overly verbose config flag documentation - self.parser._add_config_file_help = False # pylint: disable=protected-access self.detect_defaults = detect_defaults - self.args = args self.determine_verb() help1 = self.prescan_for_flag("-h", self.help_topics) @@ -355,13 +413,72 @@ class HelpfulArgumentParser(object): self.help_arg = help1 or help2 else: self.help_arg = help1 if isinstance(help1, str) else help2 - if self.help_arg is True: - # just --help with no topic; avoid argparse altogether - print(usage) - sys.exit(0) + + short_usage = self._usage_string(plugins, self.help_arg) + self.visible_topics = self.determine_help_topics(self.help_arg) self.groups = {} # elements are added by .add_group() - self.defaults = {} # elements are added by .parse_args() + self.defaults = {} # elements are added by .parse_args() + + self.parser = configargparse.ArgParser( + prog="certbot", + usage=short_usage, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + args_for_setting_config_path=["-c", "--config"], + default_config_files=flag_default("config_files"), + config_arg_help_message="path to config file (default: {0})".format( + " and ".join(flag_default("config_files")))) + + # This is the only way to turn off overly verbose config flag documentation + self.parser._add_config_file_help = False # pylint: disable=protected-access + + # Help that are synonyms for --help subcommands + COMMANDS_TOPICS = ["command", "commands", "subcommand", "subcommands", "verbs"] + def _list_subcommands(self): + longest = max(len(v) for v in VERB_HELP_MAP.keys()) + + text = "The full list of available SUBCOMMANDS is:\n\n" + for verb, props in sorted(VERB_HELP): + doc = props.get("short", "") + text += '{0:<{length}} {1}\n'.format(verb, doc, length=longest) + + text += "\nYou can get more help on a specific subcommand with --help SUBCOMMAND\n" + return text + + def _usage_string(self, plugins, help_arg): + """Make usage strings late so that plugins can be initialised late + + :param plugins: all discovered plugins + :param help_arg: False for none; True for --help; "TOPIC" for --help TOPIC + :rtype: str + :returns: a short usage string for the top of --help TOPIC) + """ + if "nginx" in plugins: + nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" + else: + nginx_doc = "(the certbot nginx plugin is not installed)" + if "apache" in plugins: + apache_doc = "--apache Use the Apache plugin for authentication & installation" + else: + apache_doc = "(the cerbot apache plugin is not installed)" + + usage = SHORT_USAGE + if help_arg == True: + print(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_USAGE) + sys.exit(0) + elif help_arg in self.COMMANDS_TOPICS: + print(usage + self._list_subcommands()) + sys.exit(0) + elif help_arg == "all": + # if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at + # the top; if we're doing --help someothertopic, it's OT so it's not + usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc) + else: + custom = VERB_HELP_MAP.get(help_arg, {}).get("usage", None) + usage = custom if custom else usage + + return usage + def parse_args(self): """Parses command line arguments and returns the result. @@ -566,7 +683,7 @@ class HelpfulArgumentParser(object): util.add_deprecated_argument( self.parser.add_argument, argument_name, num_args) - def add_group(self, topic, **kwargs): + def add_group(self, topic, verbs=(), **kwargs): """Create a new argument group. This method must be called once for every topic, however, calls @@ -574,6 +691,8 @@ class HelpfulArgumentParser(object): clarity. :param str topic: Name of the new argument group. + :param str verbs: List of subcommands that should be documented as part of + this help group / topic :returns: The new argument group. :rtype: `HelpfulArgumentGroup` @@ -581,6 +700,9 @@ class HelpfulArgumentParser(object): """ if self.visible_topics[topic]: self.groups[topic] = self.parser.add_argument_group(topic, **kwargs) + if self.help_arg: + for v in verbs: + self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v]["short"]) return HelpfulArgumentGroup(self, topic) @@ -621,32 +743,17 @@ class HelpfulArgumentParser(object): def _add_all_groups(helpful): helpful.add_group("automation", description="Arguments for automating execution & other tweaks") helpful.add_group("security", description="Security parameters & server settings") - helpful.add_group( - "testing", description="The following flags are meant for " - "testing purposes only! Do NOT change them, unless you " - "really know what you're doing!") - # VERBS - helpful.add_group( - "renew", description="The 'renew' subcommand will attempt to renew all" - " certificates (or more precisely, certificate lineages) you have" - " previously obtained if they are close to expiry, and print a" - " summary of the results. By default, 'renew' will reuse the options" - " used to create obtain or most recently successfully renew each" - " certificate lineage. You can try it with `--dry-run` first. For" - " more fine-grained control, you can renew individual lineages with" - " the `certonly` subcommand. Hooks are available to run commands" - " before and after renewal; see" - " https://certbot.eff.org/docs/using.html#renewal for more" - " information on these.") - - helpful.add_group("certonly", description="Options for modifying how a cert is obtained") - helpful.add_group("install", description="Options for modifying how a cert is deployed") - helpful.add_group("revoke", description="Options for revocation of certs") - helpful.add_group("rollback", description="Options for reverting config changes") - helpful.add_group("plugins", description='Options for the "plugins" subcommand') - helpful.add_group("config_changes", - description="Options for showing a history of config changes") + helpful.add_group("testing", + description="The following flags are meant for testing and integration purposes only.") helpful.add_group("paths", description="Arguments changing execution paths & servers") + helpful.add_group("manage", + description="Various subcommands and flags are available for managing your certificates:", + verbs=["certificates", "renew", "revoke", "rename"]) + + # VERBS + for verb, docs in VERB_HELP: + name = docs.get("realname", verb) + helpful.add_group(name, description=docs["opts"]) def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements @@ -675,7 +782,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis None, "-t", "--text", dest="text_mode", action="store_true", help=argparse.SUPPRESS) helpful.add( - [None, "automation"], "-n", "--non-interactive", "--noninteractive", + [None, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive", dest="noninteractive_mode", action="store_true", help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " @@ -688,15 +795,15 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "multiple -d flags or enter a comma separated list of domains " "as a parameter.") helpful.add( - [None, "run", "certonly"], + [None, "run", "certonly", "manage"], "--cert-name", dest="certname", metavar="CERTNAME", default=None, help="Certificate name to apply. Only one certificate name can be used " - "per Certbot run. To see certificate names, run 'certbot certificates'." + "per Certbot run. To see certificate names, run 'certbot certificates'. " "If there is no existing certificate with this name and " "domains are requested, create a new certificate with this name.") helpful.add( - "rename", + ["rename", "manage"], "--updated-cert-name", dest="new_certname", metavar="NEW_CERTNAME", default=None, help="New name for the certificate. Must be a valid filename.") @@ -728,9 +835,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="With the register verb, indicates that details associated " "with an existing registration, such as the e-mail address, " "should be updated, rather than registering a new account.") - helpful.add(None, "-m", "--email", help=config_help("email")) + helpful.add(["register", "automation"], "-m", "--email", help=config_help("email")) helpful.add( - ["automation", "renew", "certonly", "run"], + ["automation", "certonly", "run"], "--keep-until-expiring", "--keep", "--reinstall", dest="reinstall", action="store_true", help="If the requested cert matches an existing cert, always keep the " @@ -784,7 +891,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="(certbot-auto only) prevent the certbot-auto script from" " upgrading itself to newer released versions") helpful.add( - ["automation", "renew", "certonly"], + ["automation", "renew", "certonly", "run"], "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") @@ -801,11 +908,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) helpful.add( - ["certonly", "renew", "run"], "--tls-sni-01-port", type=int, + ["testing", "standalone", "apache", "nginx"], "--tls-sni-01-port", type=int, default=flag_default("tls_sni_01_port"), help=config_help("tls_sni_01_port")) helpful.add( - ["certonly", "renew", "run", "manual"], "--http-01-port", type=int, + ["testing", "standalone", "manual"], "--http-01-port", type=int, dest="http01_port", default=flag_default("http01_port"), help=config_help("http01_port")) helpful.add( @@ -859,7 +966,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") helpful.add( - ["manual", "standalone", "certonly", "renew", "run"], + ["manual", "standalone", "certonly", "renew"], "--preferred-challenges", dest="pref_challs", action=_PrefChallAction, default=[], help='A sorted, comma delimited list of the preferred challenge to ' @@ -950,10 +1057,8 @@ def _paths_parser(helpful): if verb == "help": verb = helpful.help_arg - cph = "Path to where cert is saved (with auth --csr), installed from or revoked." - section = "paths" - if verb in ("install", "revoke", "certonly"): - section = verb + cph = "Path to where cert is saved (with auth --csr), installed from, or revoked." + section = ["paths", "install", "revoke", "certonly", "manage"] if verb == "certonly": add(section, "--cert-path", type=os.path.abspath, default=flag_default("auth_cert_path"), help=cph) @@ -975,7 +1080,7 @@ def _paths_parser(helpful): default_cp = None if verb == "certonly": default_cp = flag_default("auth_chain_path") - add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath, + add(["install", "paths"], "--fullchain-path", default=default_cp, type=os.path.abspath, help="Accompanying path to a full certificate chain (cert plus chain).") add("paths", "--chain-path", default=default_cp, type=os.path.abspath, help="Accompanying path to a certificate chain.") @@ -1006,10 +1111,10 @@ def _plugins_parsing(helpful, plugins): "plugins", "--configurator", help="Name of the plugin that is " "both an authenticator and an installer. Should not be used " "together with --authenticator or --installer.") - helpful.add(["plugins", "certonly", "run", "install"], + helpful.add(["plugins", "certonly", "run", "install", "config_changes"], "--apache", action="store_true", help="Obtain and install certs using Apache") - helpful.add(["plugins", "certonly", "run", "install"], + helpful.add(["plugins", "certonly", "run", "install", "config_changes"], "--nginx", action="store_true", help="Obtain and install certs using Nginx") helpful.add(["plugins", "certonly"], "--standalone", action="store_true", diff --git a/certbot/main.py b/certbot/main.py index 2baab9670..24f38172a 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -712,6 +712,7 @@ def _handle_exception(exc_type, exc_value, trace, config): with open(logfile, "w") as logfd: traceback.print_exception( exc_type, exc_value, trace, file=logfd) + assert "--debug" not in sys.argv # config is None if this explodes except: # pylint: disable=bare-except sys.exit(tb_str) if "--debug" in sys.argv: diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 72aea50ea..2755d992c 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -84,6 +84,8 @@ class ParseTest(unittest.TestCase): self.assertTrue("--manual-test-mode" in out) self.assertTrue("--text" not in out) self.assertTrue("--dialog" not in out) + self.assertTrue("%s" not in out) + self.assertTrue("{0}" not in out) out = self._help_output(['-h', 'nginx']) if "nginx" in self.plugins: @@ -97,7 +99,7 @@ class ParseTest(unittest.TestCase): if "nginx" in self.plugins: self.assertTrue("Use the Nginx plugin" in out) else: - self.assertTrue("(nginx support is experimental" in out) + self.assertTrue("(the certbot nginx plugin is not" in out) out = self._help_output(['--help', 'plugins']) self.assertTrue("--manual-test-mode" not in out) @@ -125,8 +127,10 @@ class ParseTest(unittest.TestCase): self.assertTrue("--key-path" not in out) out = self._help_output(['-h']) - - self.assertTrue(cli.usage_strings(self.plugins)[0] in out) + self.assertTrue(cli.SHORT_USAGE in out) + self.assertTrue(cli.COMMAND_OVERVIEW[:100] in out) + self.assertTrue("%s" not in out) + self.assertTrue("{0}" not in out) def test_parse_domains(self): short_args = ['-d', 'example.com'] From ad53c80c1e95a81d5878b4d7ac0095c3b2046451 Mon Sep 17 00:00:00 2001 From: Clif Houck Date: Tue, 13 Dec 2016 16:38:57 -0600 Subject: [PATCH 295/331] Fix certbox-nginx address equality check (#3886) 0.0.0.0, *, and '' are equivalent hosts to nginx. Changes Addr object's equality testing to treat them as equal. Fixes #3855 --- certbot-nginx/certbot_nginx/obj.py | 11 +++++++++++ certbot-nginx/certbot_nginx/tests/obj_test.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 98bf86f5c..29fa976f3 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -29,10 +29,14 @@ class Addr(common.Addr): :param bool default: Whether the directive includes 'default_server' """ + UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0') + CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0] + def __init__(self, host, port, ssl, default): super(Addr, self).__init__((host, port)) self.ssl = ssl self.default = default + self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES @classmethod def fromstring(cls, str_addr): @@ -96,6 +100,13 @@ class Addr(common.Addr): def super_eq(self, other): """Check ip/port equality, with IPv6 support. """ + # If both addresses got an unspecified address, then make sure the + # host representation in each match when doing the comparison. + if self.unspecified_address and other.unspecified_address: + return common.Addr((self.CANONICAL_UNSPECIFIED_ADDRESS, + self.tup[1]), self.ipv6) == \ + common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS, + other.tup[1]), other.ipv6) # Nginx plugin currently doesn't support IPv6 but this will # future-proof it return super(Addr, self).__eq__(other) diff --git a/certbot-nginx/certbot_nginx/tests/obj_test.py b/certbot-nginx/certbot_nginx/tests/obj_test.py index b153db8d4..b0a2d5ad8 100644 --- a/certbot-nginx/certbot_nginx/tests/obj_test.py +++ b/certbot-nginx/certbot_nginx/tests/obj_test.py @@ -1,5 +1,6 @@ """Test the helper objects in certbot_nginx.obj.""" import unittest +import itertools class AddrTest(unittest.TestCase): @@ -72,6 +73,24 @@ class AddrTest(unittest.TestCase): self.assertNotEqual(self.addr1, self.addr2) self.assertFalse(self.addr1 == 3333) + def test_equivalent_any_addresses(self): + from certbot_nginx.obj import Addr + any_addresses = ("0.0.0.0:80 default_server ssl", + "80 default_server ssl", + "*:80 default_server ssl") + for first, second in itertools.combinations(any_addresses, 2): + self.assertEqual(Addr.fromstring(first), Addr.fromstring(second)) + + # Also, make sure ports are checked. + self.assertNotEqual(Addr.fromstring(any_addresses[0]), + Addr.fromstring("0.0.0.0:443 default_server ssl")) + + # And they aren't equivalent to a specified address. + for any_address in any_addresses: + self.assertNotEqual( + Addr.fromstring("192.168.1.2:80 default_server ssl"), + Addr.fromstring(any_address)) + def test_set_inclusion(self): from certbot_nginx.obj import Addr set_a = set([self.addr1, self.addr2]) From 107851ee9b5d13e42e8e76e39ffcced418a7b0e1 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 13 Dec 2016 17:32:46 -0800 Subject: [PATCH 296/331] Document defaults (#3863) * Begin fixing incorrect defaults * Fix more defaults * Make more defaults correct * Update cli-help.txt (To show what this PR does) * Lint * Extend argparse rather than vendoring it * lint * Move sample User Agent generation into the same module as UA generation * Revert cli-help.txt to previous release version * Slightly more consistent linebreaks --- certbot/cli.py | 68 +++++++++++++++++++++++--------------- certbot/client.py | 14 ++++++-- certbot/interfaces.py | 2 +- certbot/plugins/manual.py | 2 +- certbot/plugins/webroot.py | 2 +- 5 files changed, 56 insertions(+), 32 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 259953f5e..ef6257304 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -300,6 +300,21 @@ class HelpfulArgumentGroup(object): """Add a new command line argument to the argument group.""" self._parser.add(self._topic, *args, **kwargs) +class CustomHelpFormatter(argparse.HelpFormatter): + """This is a clone of ArgumentDefaultsHelpFormatter, with bugfixes. + + In particular we fix https://bugs.python.org/issue28742 + """ + + def _get_help_string(self, action): + helpstr = action.help + if '%(default)' not in action.help and '(default:' not in action.help: + if action.default != argparse.SUPPRESS: + defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] + if action.option_strings or action.nargs in defaulting_nargs: + helpstr += ' (default: %(default)s)' + return helpstr + # The attributes here are: # short: a string that will be displayed by "certbot -h commands" # opts: a string that heads the section of flags with which this command is documented, @@ -423,7 +438,7 @@ class HelpfulArgumentParser(object): self.parser = configargparse.ArgParser( prog="certbot", usage=short_usage, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, + formatter_class=CustomHelpFormatter, args_for_setting_config_path=["-c", "--config"], default_config_files=flag_default("config_files"), config_arg_help_message="path to config file (default: {0})".format( @@ -793,7 +808,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis metavar="DOMAIN", action=_DomainsAction, default=[], help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains " - "as a parameter.") + "as a parameter. (default: Ask)") helpful.add( [None, "run", "certonly", "manage"], "--cert-name", dest="certname", @@ -842,11 +857,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis dest="reinstall", action="store_true", help="If the requested cert matches an existing cert, always keep the " "existing one until it is due for renewal (for the " - "'run' subcommand this means reinstall the existing cert)") + "'run' subcommand this means reinstall the existing cert). (default: Ask)") helpful.add( "automation", "--expand", action="store_true", help="If an existing cert covers some subset of the requested names, " - "always expand and replace it with the additional names.") + "always expand and replace it with the additional names. (default: Ask)") helpful.add( "automation", "--version", action="version", version="%(prog)s {0}".format(certbot.__version__), @@ -875,7 +890,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "at this system. This option cannot be used with --csr.") helpful.add( "automation", "--agree-tos", dest="tos", action="store_true", - help="Agree to the ACME Subscriber Agreement") + help="Agree to the ACME Subscriber Agreement (default: Ask)") helpful.add( "automation", "--account", metavar="ACCOUNT_ID", help="Account ID to use") @@ -889,7 +904,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis helpful.add( "automation", "--no-self-upgrade", action="store_true", help="(certbot-auto only) prevent the certbot-auto script from" - " upgrading itself to newer released versions") + " upgrading itself to newer released versions (default: Upgrade" + " automatically)") helpful.add( ["automation", "renew", "certonly", "run"], "-q", "--quiet", dest="quiet", action="store_true", @@ -928,11 +944,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis helpful.add( "security", "--redirect", action="store_true", help="Automatically redirect all HTTP traffic to HTTPS for the newly " - "authenticated vhost.", dest="redirect", default=None) + "authenticated vhost. (default: Ask)", dest="redirect", default=None) helpful.add( "security", "--no-redirect", action="store_false", help="Do not automatically redirect all HTTP traffic to HTTPS for the newly " - "authenticated vhost.", dest="redirect", default=None) + "authenticated vhost. (default: Ask)", dest="redirect", default=None) helpful.add( "security", "--hsts", action="store_true", help="Add the Strict-Transport-Security header to every HTTP response." @@ -940,8 +956,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " Defends against SSL Stripping.", dest="hsts", default=False) helpful.add( "security", "--no-hsts", action="store_false", - help="Do not automatically add the Strict-Transport-Security header" - " to every HTTP response.", dest="hsts", default=False) + help=argparse.SUPPRESS, dest="hsts", default=False) helpful.add( "security", "--uir", action="store_true", help="Add the \"Content-Security-Policy: upgrade-insecure-requests\"" @@ -949,9 +964,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " https:// for every http:// resource.", dest="uir", default=None) helpful.add( "security", "--no-uir", action="store_false", - help="Do not automatically set the \"Content-Security-Policy:" - " upgrade-insecure-requests\" header to every HTTP response.", - dest="uir", default=None) + help=argparse.SUPPRESS, dest="uir", default=None) helpful.add( "security", "--staple-ocsp", action="store_true", help="Enables OCSP Stapling. A valid OCSP response is stapled to" @@ -959,8 +972,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis dest="staple", default=None) helpful.add( "security", "--no-staple-ocsp", action="store_false", - help="Do not automatically enable OCSP Stapling.", - dest="staple", default=None) + help=argparse.SUPPRESS, dest="staple", default=None) helpful.add( "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " @@ -1005,7 +1017,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " see if the programs being run are in the $PATH, so that mistakes can" " be caught early, even when the hooks aren't being run just yet. The" " validation is rather simplistic and fails if you use more advanced" - " shell constructs, so you can use this switch to disable it.") + " shell constructs, so you can use this switch to disable it." + " (default: False)") helpful.add_deprecated_argument("--agree-dev-preview", 0) helpful.add_deprecated_argument("--dialog", 0) @@ -1025,12 +1038,15 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis def _create_subparsers(helpful): helpful.add("config_changes", "--num", type=int, help="How many past revisions you want to be displayed") + + from certbot.client import sample_user_agent # avoid import loops helpful.add( None, "--user-agent", default=None, help="Set a custom user agent string for the client. User agent strings allow " "the CA to collect high level statistics about success rates by OS and " "plugin. If you wish to hide your server OS version from the Let's " - 'Encrypt server, set this to "".') + 'Encrypt server, set this to "". ' + '(default: {0})'.format(sample_user_agent())) helpful.add("certonly", "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER or PEM format." @@ -1103,20 +1119,18 @@ def _plugins_parsing(helpful, plugins): "a particular plugin by setting options provided below. Running " "--help will list flags specific to that plugin.") - helpful.add( - "plugins", "-a", "--authenticator", help="Authenticator plugin name.") - helpful.add( - "plugins", "-i", "--installer", help="Installer plugin name (also used to find domains).") - helpful.add( - "plugins", "--configurator", help="Name of the plugin that is " - "both an authenticator and an installer. Should not be used " - "together with --authenticator or --installer.") + helpful.add("plugins", "--configurator", + help="Name of the plugin that is both an authenticator and an installer." + " Should not be used together with --authenticator or --installer. " + "(default: Ask)") + helpful.add("plugins", "-a", "--authenticator", help="Authenticator plugin name.") + helpful.add("plugins", "-i", "--installer", + help="Installer plugin name (also used to find domains).") helpful.add(["plugins", "certonly", "run", "install", "config_changes"], "--apache", action="store_true", help="Obtain and install certs using Apache") helpful.add(["plugins", "certonly", "run", "install", "config_changes"], - "--nginx", action="store_true", - help="Obtain and install certs using Nginx") + "--nginx", action="store_true", help="Obtain and install certs using Nginx") helpful.add(["plugins", "certonly"], "--standalone", action="store_true", help='Obtain certs using a "standalone" webserver.') helpful.add(["plugins", "certonly"], "--script", action="store_true", diff --git a/certbot/client.py b/certbot/client.py index d58f9457f..6ff14bc56 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -38,11 +38,11 @@ def acme_from_config_key(config, key): "Wrangle ACME client construction" # TODO: Allow for other alg types besides RS256 net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl), - user_agent=_determine_user_agent(config)) + user_agent=determine_user_agent(config)) return acme_client.Client(config.server, key=key, net=net) -def _determine_user_agent(config): +def determine_user_agent(config): """ Set a user_agent string in the config based on the choice of plugins. (this wasn't knowable at construction time) @@ -59,6 +59,16 @@ def _determine_user_agent(config): ua = config.user_agent return ua +def sample_user_agent(): + "Document what this Certbot's user agent string will be like." + class DummyConfig(object): + "Shim for computing a sample user agent." + def __init__(self): + self.authenticator = "XXX" + self.installer = "YYY" + self.user_agent = None + return determine_user_agent(DummyConfig()) + def register(config, account_storage, tos_cb=None): """Register new account with an ACME CA. diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 8e7d887f0..6388bf936 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -201,7 +201,7 @@ class IConfig(zope.interface.Interface): """ server = zope.interface.Attribute("ACME Directory Resource URI.") email = zope.interface.Attribute( - "Email used for registration and recovery contact.") + "Email used for registration and recovery contact. (default: Ask)") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") must_staple = zope.interface.Attribute( "Adds the OCSP Must Staple extension to the certificate. " diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index c124ce048..5f933f8bc 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -98,7 +98,7 @@ s.serve_forever()" """ add("test-mode", action="store_true", help="Test mode. Executes the manual command in subprocess.") add("public-ip-logging-ok", action="store_true", - help="Automatically allows public IP logging.") + help="Automatically allows public IP logging. (default: Ask)") def prepare(self): # pylint: disable=missing-docstring,no-self-use if self.config.noninteractive_mode and not self.conf("test-mode"): diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index 2c449fdca..e9c3bcdda 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -45,7 +45,7 @@ to serve all files under specified web root ({0}).""" "times to handle different domains; each domain will have " "the webroot path that preceded it. For instance: `-w " "/var/www/example -d example.com -d www.example.com -w " - "/var/www/thing -d thing.net -d m.thing.net`") + "/var/www/thing -d thing.net -d m.thing.net` (default: Ask)") add("map", default={}, action=_WebrootMapAction, help="JSON dictionary mapping domains to webroot paths; this " "implies -d for each entry. You may need to escape this from " From 27525fb205b70db40b1f8f1264028e20d56b7dfb Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 15 Dec 2016 11:00:07 -0800 Subject: [PATCH 297/331] Use relative paths for livedir symlinks (#3914) * Use relative paths for livedir symlinks * switch directory back for the rest of the tests --- certbot/storage.py | 16 ++++++++++++++-- certbot/tests/cert_manager_test.py | 14 ++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/certbot/storage.py b/certbot/storage.py index 61ab69ff7..671922bee 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -198,6 +198,10 @@ def lineagename_for_filename(config_filename): "renewal config file name must end in .conf") return os.path.basename(config_filename[:-len(".conf")]) +def _relpath_from_file(archive_dir, from_file): + """Path to a directory from a file""" + return os.path.relpath(archive_dir, os.path.dirname(from_file)) + class RenewableCert(object): # pylint: disable=too-many-instance-attributes,too-many-public-methods @@ -305,6 +309,13 @@ class RenewableCert(object): return os.path.join( self.cli_config.default_archive_dir, self.lineagename) + def relative_archive_dir(self, from_file): + """Returns the default or specified archive directory as a relative path + + Used for creating symbolic links. + """ + return _relpath_from_file(self.archive_dir, from_file) + @property def is_test_cert(self): """Returns true if this is a test cert from a staging server.""" @@ -331,7 +342,8 @@ class RenewableCert(object): for kind in ALL_FOUR: link = getattr(self, kind) previous_link = get_link_target(link) - new_link = os.path.join(self.archive_dir, os.path.basename(previous_link)) + new_link = os.path.join(self.relative_archive_dir(link), + os.path.basename(previous_link)) os.unlink(link) os.symlink(new_link, link) @@ -856,7 +868,7 @@ class RenewableCert(object): target = dict([(kind, os.path.join(live_dir, kind + ".pem")) for kind in ALL_FOUR]) for kind in ALL_FOUR: - os.symlink(os.path.join(archive, kind + "1.pem"), + os.symlink(os.path.join(_relpath_from_file(archive, target[kind]), kind + "1.pem"), target[kind]) with open(target["cert"], "wb") as f: logger.debug("Writing certificate to %s.", target["cert"]) diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index f3569dc00..bffa5298f 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -99,10 +99,16 @@ class UpdateLiveSymlinksTest(BaseCertManagerTest): cert_manager.update_live_symlinks(self.cli_config) # check that symlinks go where they should - for domain in self.domains: - for kind in ALL_FOUR: - self.assertEqual(os.readlink(self.configs[domain][kind]), - archive_paths[domain][kind]) + prev_dir = os.getcwd() + try: + for domain in self.domains: + for kind in ALL_FOUR: + os.chdir(os.path.dirname(self.configs[domain][kind])) + self.assertEqual( + os.path.realpath(os.readlink(self.configs[domain][kind])), + os.path.realpath(archive_paths[domain][kind])) + finally: + os.chdir(prev_dir) class CertificatesTest(BaseCertManagerTest): From 16361bfd0628de97d83f91733b9db3ce1bea50e7 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 15 Dec 2016 19:41:42 -0800 Subject: [PATCH 298/331] test using os.path.sep not hardcoded / (#3920) --- certbot/tests/cert_manager_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index bffa5298f..e04e25da8 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -383,7 +383,7 @@ class RenameLineageTest(BaseCertManagerTest): mock_config.new_certname = "example.org" self.assertRaises(errors.ConfigurationError, self._call, mock_config) - mock_config.new_certname = "one/two" + mock_config.new_certname = "one{0}two".format(os.path.sep) self.assertRaises(errors.ConfigurationError, self._call, mock_config) From 81fd0cd32c1e5191621d2961a4f8cc8c1fec3160 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Thu, 15 Dec 2016 20:23:02 -0800 Subject: [PATCH 299/331] Implement delete command (#3913) * organize cert_manager.py * add delete files to cert manager and storage * add tests * add to main and cli * Clean up all related files we can find, even if some are missing. * error messages, debug logs, and remove RenewerConfiguration * add logs for failure to remove * remove renewer_config_file --- certbot/cert_manager.py | 205 +++++++++++++++------------- certbot/cli.py | 17 ++- certbot/client.py | 3 +- certbot/configuration.py | 24 ++-- certbot/constants.py | 3 - certbot/interfaces.py | 3 - certbot/main.py | 8 ++ certbot/renewal.py | 25 +--- certbot/storage.py | 140 +++++++++++++++---- certbot/tests/cert_manager_test.py | 80 ++++++----- certbot/tests/configuration_test.py | 19 +-- certbot/tests/main_test.py | 8 +- certbot/tests/renewal_test.py | 3 +- certbot/tests/storage_test.py | 104 +++++++++++++- 14 files changed, 416 insertions(+), 226 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 0f6f4c730..d8752554a 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -6,10 +6,8 @@ import pytz import traceback import zope.component -from certbot import configuration from certbot import errors from certbot import interfaces -from certbot import renewal from certbot import storage from certbot import util @@ -17,6 +15,10 @@ from certbot.display import util as display_util logger = logging.getLogger(__name__) +################### +# Commands +################### + def update_live_symlinks(config): """Update the certificate file family symlinks to use archive_dir. @@ -26,36 +28,22 @@ def update_live_symlinks(config): .. note:: This assumes that the installation is using a Reverter object. :param config: Configuration. - :type config: :class:`certbot.interfaces.IConfig` + :type config: :class:`certbot.configuration.NamespaceConfig` """ - renewer_config = configuration.RenewerConfiguration(config) - for renewal_file in renewal.renewal_conf_files(renewer_config): - storage.RenewableCert(renewal_file, - configuration.RenewerConfiguration(renewer_config), - update_symlinks=True) + for renewal_file in storage.renewal_conf_files(config): + storage.RenewableCert(renewal_file, config, update_symlinks=True) def rename_lineage(config): """Rename the specified lineage to the new name. :param config: Configuration. - :type config: :class:`certbot.interfaces.IConfig` + :type config: :class:`certbot.configuration.NamespaceConfig` """ disp = zope.component.getUtility(interfaces.IDisplay) - renewer_config = configuration.RenewerConfiguration(config) - certname = config.certname - if not certname: - filenames = renewal.renewal_conf_files(renewer_config) - choices = [storage.lineagename_for_filename(name) for name in filenames] - if not choices: - raise errors.Error("No existing certificates found.") - code, index = disp.menu("Which certificate would you like to rename?", - choices, ok_label="Select", flag="--cert-name") - if code != display_util.OK or not index in range(0, len(choices)): - raise errors.Error("User ended interaction.") - certname = choices[index] + certname = _get_certname(config, "rename") new_certname = config.new_certname if not new_certname: @@ -68,10 +56,110 @@ def rename_lineage(config): if not lineage: raise errors.ConfigurationError("No existing certificate with name " "{0} found.".format(certname)) - storage.rename_renewal_config(certname, new_certname, renewer_config) + storage.rename_renewal_config(certname, new_certname, config) disp.notification("Successfully renamed {0} to {1}." .format(certname, new_certname), pause=False) +def certificates(config): + """Display information about certs configured with Certbot + + :param config: Configuration. + :type config: :class:`certbot.configuration.NamespaceConfig` + """ + parsed_certs = [] + parse_failures = [] + for renewal_file in storage.renewal_conf_files(config): + try: + renewal_candidate = storage.RenewableCert(renewal_file, config) + parsed_certs.append(renewal_candidate) + except Exception as e: # pylint: disable=broad-except + logger.warning("Renewal configuration file %s produced an " + "unexpected error: %s. Skipping.", renewal_file, e) + logger.debug("Traceback was:\n%s", traceback.format_exc()) + parse_failures.append(renewal_file) + + # Describe all the certs + _describe_certs(parsed_certs, parse_failures) + +def delete(config): + """Delete Certbot files associated with a certificate lineage.""" + certname = _get_certname(config, "delete") + storage.delete_files(config, certname) + disp = zope.component.getUtility(interfaces.IDisplay) + disp.notification("Deleted all files relating to certificate {0}." + .format(certname), pause=False) + +################### +# Public Helpers +################### + +def lineage_for_certname(config, certname): + """Find a lineage object with name certname.""" + def update_cert_for_name_match(candidate_lineage, rv): + """Return cert if it has name certname, else return rv + """ + matching_lineage_name_cert = rv + if candidate_lineage.lineagename == certname: + matching_lineage_name_cert = candidate_lineage + return matching_lineage_name_cert + return _search_lineages(config, update_cert_for_name_match, None) + +def domains_for_certname(config, certname): + """Find the domains in the cert with name certname.""" + def update_domains_for_name_match(candidate_lineage, rv): + """Return domains if certname matches, else return rv + """ + matching_domains = rv + if candidate_lineage.lineagename == certname: + matching_domains = candidate_lineage.names() + return matching_domains + return _search_lineages(config, update_domains_for_name_match, None) + +def find_duplicative_certs(config, domains): + """Find existing certs that duplicate the request.""" + def update_certs_for_domain_matches(candidate_lineage, rv): + """Return cert as identical_names_cert if it matches, + or subset_names_cert if it matches as subset + """ + # TODO: Handle these differently depending on whether they are + # expired or still valid? + identical_names_cert, subset_names_cert = rv + candidate_names = set(candidate_lineage.names()) + if candidate_names == set(domains): + identical_names_cert = candidate_lineage + elif candidate_names.issubset(set(domains)): + # This logic finds and returns the largest subset-names cert + # in the case where there are several available. + if subset_names_cert is None: + subset_names_cert = candidate_lineage + elif len(candidate_names) > len(subset_names_cert.names()): + subset_names_cert = candidate_lineage + return (identical_names_cert, subset_names_cert) + + return _search_lineages(config, update_certs_for_domain_matches, (None, None)) + + +################### +# Private Helpers +################### + +def _get_certname(config, verb): + """Get certname from flag, interactively, or error out. + """ + certname = config.certname + if not certname: + disp = zope.component.getUtility(interfaces.IDisplay) + filenames = storage.renewal_conf_files(config) + choices = [storage.lineagename_for_filename(name) for name in filenames] + if not choices: + raise errors.Error("No existing certificates found.") + code, index = disp.menu("Which certificate would you like to {0}?".format(verb), + choices, ok_label="Select", flag="--cert-name") + if code != display_util.OK or not index in range(0, len(choices)): + raise errors.Error("User ended interaction.") + certname = choices[index] + return certname + def _report_lines(msgs): """Format a results report for a category of single-line renewal outcomes""" return " " + "\n ".join(str(msg) for msg in msgs) @@ -126,42 +214,18 @@ def _describe_certs(parsed_certs, parse_failures): disp = zope.component.getUtility(interfaces.IDisplay) disp.notification("\n".join(out), pause=False, wrap=False) -def certificates(config): - """Display information about certs configured with Certbot - - :param config: Configuration. - :type config: :class:`certbot.interfaces.IConfig` - """ - renewer_config = configuration.RenewerConfiguration(config) - parsed_certs = [] - parse_failures = [] - for renewal_file in renewal.renewal_conf_files(renewer_config): - try: - renewal_candidate = storage.RenewableCert(renewal_file, - configuration.RenewerConfiguration(config)) - parsed_certs.append(renewal_candidate) - except Exception as e: # pylint: disable=broad-except - logger.warning("Renewal configuration file %s produced an " - "unexpected error: %s. Skipping.", renewal_file, e) - logger.debug("Traceback was:\n%s", traceback.format_exc()) - parse_failures.append(renewal_file) - - # Describe all the certs - _describe_certs(parsed_certs, parse_failures) - -def _search_lineages(config, func, initial_rv): +def _search_lineages(cli_config, func, initial_rv): """Iterate func over unbroken lineages, allowing custom return conditions. Allows flexible customization of return values, including multiple return values and complex checks. """ - cli_config = configuration.RenewerConfiguration(config) configs_dir = cli_config.renewal_configs_dir # Verify the directory is there util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) rv = initial_rv - for renewal_file in renewal.renewal_conf_files(cli_config): + for renewal_file in storage.renewal_conf_files(cli_config): try: candidate_lineage = storage.RenewableCert(renewal_file, cli_config) except (errors.CertStorageError, IOError): @@ -170,48 +234,3 @@ def _search_lineages(config, func, initial_rv): continue rv = func(candidate_lineage, rv) return rv - -def lineage_for_certname(config, certname): - """Find a lineage object with name certname.""" - def update_cert_for_name_match(candidate_lineage, rv): - """Return cert if it has name certname, else return rv - """ - matching_lineage_name_cert = rv - if candidate_lineage.lineagename == certname: - matching_lineage_name_cert = candidate_lineage - return matching_lineage_name_cert - return _search_lineages(config, update_cert_for_name_match, None) - -def domains_for_certname(config, certname): - """Find the domains in the cert with name certname.""" - def update_domains_for_name_match(candidate_lineage, rv): - """Return domains if certname matches, else return rv - """ - matching_domains = rv - if candidate_lineage.lineagename == certname: - matching_domains = candidate_lineage.names() - return matching_domains - return _search_lineages(config, update_domains_for_name_match, None) - -def find_duplicative_certs(config, domains): - """Find existing certs that duplicate the request.""" - def update_certs_for_domain_matches(candidate_lineage, rv): - """Return cert as identical_names_cert if it matches, - or subset_names_cert if it matches as subset - """ - # TODO: Handle these differently depending on whether they are - # expired or still valid? - identical_names_cert, subset_names_cert = rv - candidate_names = set(candidate_lineage.names()) - if candidate_names == set(domains): - identical_names_cert = candidate_lineage - elif candidate_names.issubset(set(domains)): - # This logic finds and returns the largest subset-names cert - # in the case where there are several available. - if subset_names_cert is None: - subset_names_cert = candidate_lineage - elif len(candidate_names) > len(subset_names_cert.names()): - subset_names_cert = candidate_lineage - return (identical_names_cert, subset_names_cert) - - return _search_lineages(config, update_certs_for_domain_matches, (None, None)) diff --git a/certbot/cli.py b/certbot/cli.py index ef6257304..89c171829 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -82,6 +82,7 @@ manage certificates: certificates Display information about certs you have from Certbot revoke Revoke a certificate (supply --cert-path) rename Rename a certificate + delete Delete a certificate manage your account with Let's Encrypt: register Create a Let's Encrypt ACME account @@ -352,13 +353,17 @@ VERB_HELP = [ "short": "List all certificates managed by Certbot", "opts": "List all certificates managed by Certbot" }), + ("delete", { + "short": "Clean up all files related to a certificate", + "opts": "Options for deleting a certificate" + }), ("revoke", { "short": "Revoke a certificate specified with --cert-path", "opts": "Options for revocation of certs" }), ("rename", { "short": "Change a certificate's name (for management purposes)", - "opts": "Options changing certificate names" + "opts": "Options for changing certificate names" }), ("register", { "short": "Register for account with Let's Encrypt / other ACME server", @@ -410,7 +415,8 @@ class HelpfulArgumentParser(object): "register": main.register, "renew": main.renew, "revoke": main.revoke, "rollback": main.rollback, "everything": main.run, "update_symlinks": main.update_symlinks, - "certificates": main.certificates, "rename": main.rename} + "certificates": main.certificates, "rename": main.rename, + "delete": main.delete} # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) @@ -763,7 +769,7 @@ def _add_all_groups(helpful): helpful.add_group("paths", description="Arguments changing execution paths & servers") helpful.add_group("manage", description="Various subcommands and flags are available for managing your certificates:", - verbs=["certificates", "renew", "revoke", "rename"]) + verbs=["certificates", "delete", "renew", "revoke", "rename"]) # VERBS for verb, docs in VERB_HELP: @@ -810,13 +816,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "multiple -d flags or enter a comma separated list of domains " "as a parameter. (default: Ask)") helpful.add( - [None, "run", "certonly", "manage"], + [None, "run", "certonly", "manage", "rename", "delete"], "--cert-name", dest="certname", metavar="CERTNAME", default=None, help="Certificate name to apply. Only one certificate name can be used " "per Certbot run. To see certificate names, run 'certbot certificates'. " - "If there is no existing certificate with this name and " - "domains are requested, create a new certificate with this name.") + "When creating a new certificate, specifies the new certificate's name.") helpful.add( ["rename", "manage"], "--updated-cert-name", dest="new_certname", diff --git a/certbot/client.py b/certbot/client.py index 6ff14bc56..cfd2b8487 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -15,7 +15,6 @@ import certbot from certbot import account from certbot import auth_handler -from certbot import configuration from certbot import constants from certbot import crypto_util from certbot import errors @@ -307,7 +306,7 @@ class Client(object): new_name, OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped), key.pem, crypto_util.dump_pyopenssl_chain(chain), - configuration.RenewerConfiguration(self.config.namespace)) + self.config) def save_certificate(self, certr, chain_cert, cert_path, chain_path, fullchain_path): diff --git a/certbot/configuration.py b/certbot/configuration.py index 1d4243272..d25378922 100644 --- a/certbot/configuration.py +++ b/certbot/configuration.py @@ -25,9 +25,16 @@ class NamespaceConfig(object): - `csr_dir` - `in_progress_dir` - `key_dir` - - `renewer_config_file` - `temp_checkpoint_dir` + And the following paths are dynamically resolved using + :attr:`~certbot.interfaces.IConfig.config_dir` and relative + paths defined in :py:mod:`certbot.constants`: + + - `default_archive_dir` + - `live_dir` + - `renewal_configs_dir` + :ivar namespace: Namespace typically produced by :meth:`argparse.ArgumentParser.parse_args`. :type namespace: :class:`argparse.Namespace` @@ -85,16 +92,6 @@ class NamespaceConfig(object): new_ns = copy.deepcopy(self.namespace) return type(self)(new_ns) - -class RenewerConfiguration(object): - """Configuration wrapper for renewer.""" - - def __init__(self, namespace): - self.namespace = namespace - - def __getattr__(self, name): - return getattr(self.namespace, name) - @property def default_archive_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR) @@ -108,11 +105,6 @@ class RenewerConfiguration(object): return os.path.join( self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR) - @property - def renewer_config_file(self): # pylint: disable=missing-docstring - return os.path.join( - self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME) - def check_config_sanity(config): """Validate command line options and display error message if diff --git a/certbot/constants.py b/certbot/constants.py index 117301380..2e74cfd86 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -92,6 +92,3 @@ TEMP_CHECKPOINT_DIR = "temp_checkpoint" RENEWAL_CONFIGS_DIR = "renewal" """Renewal configs directory, relative to `IConfig.config_dir`.""" - -RENEWER_CONFIG_FILENAME = "renewer.conf" -"""Renewer config file name (relative to `IConfig.config_dir`).""" diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 6388bf936..f2b3faf21 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -223,9 +223,6 @@ class IConfig(zope.interface.Interface): temp_checkpoint_dir = zope.interface.Attribute( "Temporary checkpoint directory.") - renewer_config_file = zope.interface.Attribute( - "Location of renewal configuration file.") - no_verify_ssl = zope.interface.Attribute( "Disable verification of the ACME server's certificate.") tls_sni_01_port = zope.interface.Attribute( diff --git a/certbot/main.py b/certbot/main.py index 24f38172a..6a1653193 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -511,6 +511,14 @@ def rename(config, unused_plugins): """ cert_manager.rename_lineage(config) +def delete(config, unused_plugins): + """Delete a certificate + + Use the information in the config file to delete an existing + lineage. + """ + cert_manager.delete(config) + def certificates(config, unused_plugins): """Display information about certs configured with Certbot """ diff --git a/certbot/renewal.py b/certbot/renewal.py index a057a63a9..c9eca9a3a 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -1,7 +1,6 @@ """Functionality for autorenewal and associated juggling of configurations""" from __future__ import print_function import copy -import glob import logging import os import traceback @@ -11,7 +10,6 @@ import zope.component import OpenSSL -from certbot import configuration from certbot import cli from certbot import crypto_util @@ -34,17 +32,6 @@ STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent", INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"] -def renewal_conf_files(config): - """Return /path/to/*.conf in the renewal conf directory""" - return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) - -def renewal_file_for_certname(config, certname): - """Return /path/to/certname.conf in the renewal conf directory""" - path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname)) - if not os.path.exists(path): - raise errors.CertStorageError("No certificate found with name {0}.".format(certname)) - return path - def _reconstitute(config, full_path): """Try to instantiate a RenewableCert, updating config with relevant items. @@ -63,8 +50,7 @@ def _reconstitute(config, full_path): """ try: - renewal_candidate = storage.RenewableCert( - full_path, configuration.RenewerConfiguration(config)) + renewal_candidate = storage.RenewableCert(full_path, config) except (errors.CertStorageError, IOError) as exc: logger.warning(exc) logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) @@ -246,9 +232,8 @@ def renew_cert(config, le_client, lineage): new_cert = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped) new_chain = crypto_util.dump_pyopenssl_chain(new_chain) - renewal_conf = configuration.RenewerConfiguration(config.namespace) # TODO: Check return value of save_successor - lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, renewal_conf) + lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, config) lineage.update_all_links_to(lineage.latest_common_version()) hooks.renew_hook(config, lineage.names(), lineage.live_dir) @@ -317,12 +302,10 @@ def handle_renewal_request(config): "command. The renew verb may provide other options " "for selecting certificates to renew in the future.") - renewer_config = configuration.RenewerConfiguration(config) - if config.certname: - conf_files = [renewal_file_for_certname(renewer_config, config.certname)] + conf_files = [storage.renewal_file_for_certname(config, config.certname)] else: - conf_files = renewal_conf_files(renewer_config) + conf_files = storage.renewal_conf_files(config) renew_successes = [] renew_failures = [] diff --git a/certbot/storage.py b/certbot/storage.py index 671922bee..e4fc21a85 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -1,5 +1,6 @@ """Renewable certificates storage.""" import datetime +import glob import logging import os import re @@ -7,6 +8,7 @@ import re import configobj import parsedatetime import pytz +import shutil import six import certbot @@ -20,9 +22,22 @@ from certbot import util logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") +README = "README" CURRENT_VERSION = util.get_strict_version(certbot.__version__) +def renewal_conf_files(config): + """Return /path/to/*.conf in the renewal conf directory""" + return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) + +def renewal_file_for_certname(config, certname): + """Return /path/to/certname.conf in the renewal conf directory""" + path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname)) + if not os.path.exists(path): + raise errors.CertStorageError("No certificate found with name {0} (expected " + "{1}).".format(certname, path)) + return path + def config_with_defaults(config=None): """Merge supplied config, if provided, on top of builtin defaults.""" defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS) @@ -99,13 +114,11 @@ def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_d def rename_renewal_config(prev_name, new_name, cli_config): """Renames cli_config.certname's config to cli_config.new_certname. - :param .RenewerConfiguration cli_config: parsed command line + :param .NamespaceConfig cli_config: parsed command line arguments """ - prev_filename = os.path.join( - cli_config.renewal_configs_dir, prev_name) + ".conf" - new_filename = os.path.join( - cli_config.renewal_configs_dir, new_name) + ".conf" + prev_filename = renewal_filename_for_lineagename(cli_config, prev_name) + new_filename = renewal_filename_for_lineagename(cli_config, new_name) if os.path.exists(new_filename): raise errors.ConfigurationError("The new certificate name " "is already in use.") @@ -122,15 +135,14 @@ def update_configuration(lineagename, archive_dir, target, cli_config): :param str lineagename: Name of the lineage being modified :param str archive_dir: Absolute path to the archive directory :param dict target: Maps ALL_FOUR to their symlink paths - :param .RenewerConfiguration cli_config: parsed command line + :param .NamespaceConfig cli_config: parsed command line arguments :returns: Configuration object for the updated config file :rtype: configobj.ConfigObj """ - config_filename = os.path.join( - cli_config.renewal_configs_dir, lineagename) + ".conf" + config_filename = renewal_filename_for_lineagename(cli_config, lineagename) temp_filename = config_filename + ".new" # If an existing tempfile exists, delete it @@ -198,10 +210,98 @@ def lineagename_for_filename(config_filename): "renewal config file name must end in .conf") return os.path.basename(config_filename[:-len(".conf")]) +def renewal_filename_for_lineagename(config, lineagename): + """Returns the lineagename for a configuration filename. + """ + return os.path.join(config.renewal_configs_dir, lineagename) + ".conf" + def _relpath_from_file(archive_dir, from_file): """Path to a directory from a file""" return os.path.relpath(archive_dir, os.path.dirname(from_file)) +def _full_archive_path(config_obj, cli_config, lineagename): + """Returns the full archive path for a lineagename + + Uses cli_config to determine archive path if not available from config_obj. + + :param configobj.ConfigObj config_obj: Renewal conf file contents (can be None) + :param configuration.NamespaceConfig cli_config: Main config file + :param str lineagename: Certificate name + """ + if config_obj and "archive_dir" in config_obj: + return config_obj["archive_dir"] + else: + return os.path.join(cli_config.default_archive_dir, lineagename) + +def _full_live_path(cli_config, lineagename): + """Returns the full default live path for a lineagename""" + return os.path.join(cli_config.live_dir, lineagename) + +def delete_files(config, certname): + """Delete all files related to the certificate. + + If some files are not found, ignore them and continue. + """ + renewal_filename = renewal_file_for_certname(config, certname) + # file exists + full_default_archive_dir = _full_archive_path(None, config, certname) + full_default_live_dir = _full_live_path(config, certname) + try: + renewal_config = configobj.ConfigObj(renewal_filename) + except configobj.ConfigObjError: + # config is corrupted + logger.warning("Could not parse %s. You may wish to manually " + "delete the contents of %s and %s.", renewal_filename, + full_default_live_dir, full_default_archive_dir) + raise errors.CertStorageError( + "error parsing {0}".format(renewal_filename)) + finally: + # we couldn't read it, but let's at least delete it + # if this was going to fail, it already would have. + os.remove(renewal_filename) + logger.debug("Removed %s", renewal_filename) + + # cert files and (hopefully) live directory + # it's not guaranteed that the files are in our default storage + # structure. so, first delete the cert files. + directory_names = set() + for kind in ALL_FOUR: + link = renewal_config.get(kind) + try: + os.remove(link) + logger.debug("Removed %s", link) + except OSError: + logger.debug("Unable to delete %s", link) + directory = os.path.dirname(link) + directory_names.add(directory) + + # if all four were in the same directory, and the only thing left + # is the README file (or nothing), delete that directory. + # this will be wrong in very few but some cases. + if len(directory_names) == 1: + # delete the README file + directory = directory_names.pop() + readme_path = os.path.join(directory, README) + try: + os.remove(readme_path) + logger.debug("Removed %s", readme_path) + except OSError: + logger.debug("Unable to delete %s", readme_path) + # if it's now empty, delete the directory + try: + os.rmdir(directory) # only removes empty directories + logger.debug("Removed %s", directory) + except OSError: + logger.debug("Unable to remove %s; may not be empty.", directory) + + # archive directory + try: + archive_path = _full_archive_path(renewal_config, config, certname) + shutil.rmtree(archive_path) + logger.debug("Removed %s", archive_path) + except OSError: + logger.debug("Unable to remove %s", archive_path) + class RenewableCert(object): # pylint: disable=too-many-instance-attributes,too-many-public-methods @@ -244,7 +344,7 @@ class RenewableCert(object): :param str config_filename: the path to the renewal config file that defines this lineage. - :param .RenewerConfiguration: parsed command line arguments + :param .NamespaceConfig: parsed command line arguments :raises .CertStorageError: if the configuration file's name didn't end in ".conf", or the file is missing or broken. @@ -303,11 +403,8 @@ class RenewableCert(object): @property def archive_dir(self): """Returns the default or specified archive directory""" - if "archive_dir" in self.configuration: - return self.configuration["archive_dir"] - else: - return os.path.join( - self.cli_config.default_archive_dir, self.lineagename) + return _full_archive_path(self.configuration, + self.cli_config, self.lineagename) def relative_archive_dir(self, from_file): """Returns the default or specified archive directory as a relative path @@ -827,7 +924,7 @@ class RenewableCert(object): :param str cert: the initial certificate version in PEM format :param str privkey: the private key in PEM format :param str chain: the certificate chain in PEM format - :param .RenewerConfiguration cli_config: parsed command line + :param .NamespaceConfig cli_config: parsed command line arguments :returns: the newly-created RenewalCert object @@ -843,16 +940,13 @@ class RenewableCert(object): logger.debug("Creating directory %s.", i) config_file, config_filename = util.unique_lineage_name( cli_config.renewal_configs_dir, lineagename) - if not config_filename.endswith(".conf"): - raise errors.CertStorageError( - "renewal config file name must end in .conf") # Determine where on disk everything will go # lineagename will now potentially be modified based on which # renewal configuration file could actually be created - lineagename = os.path.basename(config_filename)[:-len(".conf")] - archive = os.path.join(cli_config.default_archive_dir, lineagename) - live_dir = os.path.join(cli_config.live_dir, lineagename) + lineagename = lineagename_for_filename(config_filename) + archive = _full_archive_path(None, cli_config, lineagename) + live_dir = _full_live_path(cli_config, lineagename) if os.path.exists(archive): raise errors.CertStorageError( "archive directory exists for " + lineagename) @@ -887,7 +981,7 @@ class RenewableCert(object): f.write(cert + chain) # Write a README file to the live directory - readme_path = os.path.join(live_dir, "README") + readme_path = os.path.join(live_dir, README) with open(readme_path, "w") as f: logger.debug("Writing README to %s.", readme_path) f.write("This directory contains your keys and certificates.\n\n" @@ -928,7 +1022,7 @@ class RenewableCert(object): :param str new_privkey: the new private key, in PEM format, or ``None``, if the private key has not changed :param str new_chain: the new chain, in PEM format - :param .RenewerConfiguration cli_config: parsed command line + :param .NamespaceConfig cli_config: parsed command line arguments :returns: the new version number that was created diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index e04e25da8..ae673fba1 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -25,12 +25,12 @@ class BaseCertManagerTest(unittest.TestCase): os.makedirs(os.path.join(self.tempdir, "renewal")) - self.cli_config = mock.MagicMock( + self.cli_config = configuration.NamespaceConfig(mock.MagicMock( config_dir=self.tempdir, work_dir=self.tempdir, logs_dir=self.tempdir, quiet=False, - ) + )) self.domains = { "example.org": None, @@ -47,7 +47,7 @@ class BaseCertManagerTest(unittest.TestCase): junk.close() def _set_up_config(self, domain, custom_archive): - # TODO: maybe provide RenewerConfiguration.make_dirs? + # TODO: maybe provide NamespaceConfig.make_dirs? # TODO: main() should create those dirs, c.f. #902 os.makedirs(os.path.join(self.tempdir, "live", domain)) config = configobj.ConfigObj() @@ -111,6 +111,21 @@ class UpdateLiveSymlinksTest(BaseCertManagerTest): os.chdir(prev_dir) +class DeleteTest(storage_test.BaseRenewableCertTest): + """Tests for certbot.cert_manager.delete + """ + @mock.patch('zope.component.getUtility') + @mock.patch('certbot.cert_manager.lineage_for_certname') + @mock.patch('certbot.storage.delete_files') + def test_delete(self, mock_delete_files, mock_lineage_for_certname, unused_get_utility): + """Test delete""" + mock_lineage_for_certname.return_value = self.test_rc + self.cli_config.certname = "example.org" + from certbot import cert_manager + cert_manager.delete(self.cli_config) + self.assertTrue(mock_delete_files.called) + + class CertificatesTest(BaseCertManagerTest): """Tests for certbot.cert_manager.certificates """ @@ -151,12 +166,12 @@ class CertificatesTest(BaseCertManagerTest): def test_certificates_no_files(self, mock_utility, mock_logger): tempdir = tempfile.mkdtemp() - cli_config = mock.MagicMock( + cli_config = configuration.NamespaceConfig(mock.MagicMock( config_dir=tempdir, work_dir=tempdir, logs_dir=tempdir, quiet=False, - ) + )) os.makedirs(os.path.join(tempdir, "renewal")) self._certificates(cli_config) @@ -202,90 +217,85 @@ class CertificatesTest(BaseCertManagerTest): self.assertTrue('INVALID: TEST CERT' in out) -class SearchLineagesTest(unittest.TestCase): +class SearchLineagesTest(BaseCertManagerTest): """Tests for certbot.cert_manager._search_lineages.""" - @mock.patch('certbot.configuration.RenewerConfiguration') @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.renewal_conf_files') @mock.patch('certbot.storage.RenewableCert') def test_cert_storage_error(self, mock_renewable_cert, mock_renewal_conf_files, - mock_make_or_verify_dir, mock_renewer_config): + mock_make_or_verify_dir): mock_renewal_conf_files.return_value = ["badfile"] mock_renewable_cert.side_effect = errors.CertStorageError from certbot import cert_manager # pylint: disable=protected-access - self.assertEqual(cert_manager._search_lineages(None, lambda x: x, "check"), "check") + self.assertEqual(cert_manager._search_lineages(self.cli_config, lambda x: x, "check"), + "check") self.assertTrue(mock_make_or_verify_dir.called) - self.assertTrue(mock_renewer_config) -class LineageForCertnameTest(unittest.TestCase): +class LineageForCertnameTest(BaseCertManagerTest): """Tests for certbot.cert_manager.lineage_for_certname""" - @mock.patch('certbot.configuration.RenewerConfiguration') @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.renewal_conf_files') @mock.patch('certbot.storage.RenewableCert') def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files, - mock_make_or_verify_dir, mock_renewer_config): + mock_make_or_verify_dir): mock_renewal_conf_files.return_value = ["somefile.conf"] mock_match = mock.Mock(lineagename="example.com") mock_renewable_cert.return_value = mock_match from certbot import cert_manager - self.assertEqual(cert_manager.lineage_for_certname(None, "example.com"), mock_match) + self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"), + mock_match) self.assertTrue(mock_make_or_verify_dir.called) - self.assertTrue(mock_renewer_config) - @mock.patch('certbot.configuration.RenewerConfiguration') @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.renewal_conf_files') @mock.patch('certbot.storage.RenewableCert') def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files, - mock_make_or_verify_dir, mock_renewer_config): + mock_make_or_verify_dir): mock_renewal_conf_files.return_value = ["somefile.conf"] mock_match = mock.Mock(lineagename="other.com") mock_renewable_cert.return_value = mock_match from certbot import cert_manager - self.assertEqual(cert_manager.lineage_for_certname(None, "example.com"), None) + self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"), + None) self.assertTrue(mock_make_or_verify_dir.called) - self.assertTrue(mock_renewer_config) -class DomainsForCertnameTest(unittest.TestCase): +class DomainsForCertnameTest(BaseCertManagerTest): """Tests for certbot.cert_manager.domains_for_certname""" - @mock.patch('certbot.configuration.RenewerConfiguration') @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.renewal_conf_files') @mock.patch('certbot.storage.RenewableCert') def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files, - mock_make_or_verify_dir, mock_renewer_config): + mock_make_or_verify_dir): mock_renewal_conf_files.return_value = ["somefile.conf"] mock_match = mock.Mock(lineagename="example.com") domains = ["example.com", "example.org"] mock_match.names.return_value = domains mock_renewable_cert.return_value = mock_match from certbot import cert_manager - self.assertEqual(cert_manager.domains_for_certname(None, "example.com"), domains) + self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "example.com"), + domains) self.assertTrue(mock_make_or_verify_dir.called) - self.assertTrue(mock_renewer_config) - @mock.patch('certbot.configuration.RenewerConfiguration') @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.renewal_conf_files') @mock.patch('certbot.storage.RenewableCert') def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files, - mock_make_or_verify_dir, mock_renewer_config): + mock_make_or_verify_dir): mock_renewal_conf_files.return_value = ["somefile.conf"] mock_match = mock.Mock(lineagename="example.com") domains = ["example.com", "example.org"] mock_match.names.return_value = domains mock_renewable_cert.return_value = mock_match from certbot import cert_manager - self.assertEqual(cert_manager.domains_for_certname(None, "other.com"), None) + self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"), + None) self.assertTrue(mock_make_or_verify_dir.called) - self.assertTrue(mock_renewer_config) class RenameLineageTest(BaseCertManagerTest): @@ -293,7 +303,7 @@ class RenameLineageTest(BaseCertManagerTest): def setUp(self): super(RenameLineageTest, self).setUp() - self.mock_config = configuration.RenewerConfiguration( + self.mock_config = configuration.NamespaceConfig( namespace=mock.MagicMock( config_dir=self.tempdir, work_dir=self.tempdir, @@ -307,7 +317,7 @@ class RenameLineageTest(BaseCertManagerTest): from certbot import cert_manager return cert_manager.rename_lineage(*args, **kwargs) - @mock.patch('certbot.renewal.renewal_conf_files') + @mock.patch('certbot.storage.renewal_conf_files') @mock.patch('certbot.main.zope.component.getUtility') def test_no_certname(self, mock_get_utility, mock_renewal_conf_files): mock_config = mock.Mock(certname=None, new_certname="two") diff --git a/certbot/tests/configuration_test.py b/certbot/tests/configuration_test.py index 5e59d0b86..183d6a95c 100644 --- a/certbot/tests/configuration_test.py +++ b/certbot/tests/configuration_test.py @@ -88,31 +88,19 @@ class NamespaceConfigTest(unittest.TestCase): self.assertTrue(os.path.isabs(config.key_dir)) self.assertTrue(os.path.isabs(config.temp_checkpoint_dir)) - -class RenewerConfigurationTest(unittest.TestCase): - """Test for certbot.configuration.RenewerConfiguration.""" - - def setUp(self): - self.namespace = mock.MagicMock(config_dir='/tmp/config') - from certbot.configuration import RenewerConfiguration - self.config = RenewerConfiguration(self.namespace) - @mock.patch('certbot.configuration.constants') - def test_dynamic_dirs(self, constants): + def test_renewal_dynamic_dirs(self, constants): constants.ARCHIVE_DIR = 'a' constants.LIVE_DIR = 'l' constants.RENEWAL_CONFIGS_DIR = 'renewal_configs' - constants.RENEWER_CONFIG_FILENAME = 'r.conf' self.assertEqual(self.config.default_archive_dir, '/tmp/config/a') self.assertEqual(self.config.live_dir, '/tmp/config/l') self.assertEqual( self.config.renewal_configs_dir, '/tmp/config/renewal_configs') - self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') - def test_absolute_paths(self): + def test_renewal_absolute_paths(self): from certbot.configuration import NamespaceConfig - from certbot.configuration import RenewerConfiguration config_base = "foo" work_base = "bar" @@ -125,12 +113,11 @@ class RenewerConfigurationTest(unittest.TestCase): mock_namespace.config_dir = config_base mock_namespace.work_dir = work_base mock_namespace.logs_dir = logs_base - config = RenewerConfiguration(NamespaceConfig(mock_namespace)) + config = NamespaceConfig(mock_namespace) self.assertTrue(os.path.isabs(config.default_archive_dir)) self.assertTrue(os.path.isabs(config.live_dir)) self.assertTrue(os.path.isabs(config.renewal_configs_dir)) - self.assertTrue(os.path.isabs(config.renewer_config_file)) if __name__ == '__main__': diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index a0d6cc418..3665f09bb 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -567,6 +567,11 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call_no_clientmock(['certificates']) self.assertEqual(1, mock_cert_manager.call_count) + @mock.patch('certbot.cert_manager.delete') + def test_delete(self, mock_cert_manager): + self._call_no_clientmock(['delete']) + self.assertEqual(1, mock_cert_manager.call_count) + def test_plugins(self): flags = ['--init', '--prepare', '--authenticators', '--installers'] for args in itertools.chain( @@ -856,8 +861,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods rc_path = test_util.make_lineage(self, 'sample-renewal-ancient.conf') args = mock.MagicMock(account=None, email=None, webroot_path=None) config = configuration.NamespaceConfig(args) - lineage = storage.RenewableCert(rc_path, - configuration.RenewerConfiguration(config)) + lineage = storage.RenewableCert(rc_path, config) renewalparams = lineage.configuration["renewalparams"] # pylint: disable=protected-access renewal._restore_webroot_config(config, renewalparams) diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py index 207b70041..8155595c2 100644 --- a/certbot/tests/renewal_test.py +++ b/certbot/tests/renewal_test.py @@ -21,8 +21,7 @@ class RenewalTest(unittest.TestCase): rc_path = util.make_lineage(self, 'sample-renewal-ancient.conf') args = mock.MagicMock(account=None, email=None, webroot_path=None) config = configuration.NamespaceConfig(args) - lineage = storage.RenewableCert( - rc_path, configuration.RenewerConfiguration(config)) + lineage = storage.RenewableCert(rc_path, config) renewalparams = lineage.configuration["renewalparams"] # pylint: disable=protected-access from certbot import renewal diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index ebe7d2243..a1fda6535 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -49,7 +49,7 @@ class BaseRenewableCertTest(unittest.TestCase): from certbot import storage self.tempdir = tempfile.mkdtemp() - self.cli_config = configuration.RenewerConfiguration( + self.cli_config = configuration.NamespaceConfig( namespace=mock.MagicMock( config_dir=self.tempdir, work_dir=self.tempdir, @@ -57,16 +57,22 @@ class BaseRenewableCertTest(unittest.TestCase): ) ) - # TODO: maybe provide RenewerConfiguration.make_dirs? + # TODO: maybe provide NamespaceConfig.make_dirs? # TODO: main() should create those dirs, c.f. #902 os.makedirs(os.path.join(self.tempdir, "live", "example.org")) - os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) + archive_path = os.path.join(self.tempdir, "archive", "example.org") + os.makedirs(archive_path) os.makedirs(os.path.join(self.tempdir, "renewal")) config = configobj.ConfigObj() for kind in ALL_FOUR: - config[kind] = os.path.join(self.tempdir, "live", "example.org", + kind_path = os.path.join(self.tempdir, "live", "example.org", kind + ".pem") + config[kind] = kind_path + with open(os.path.join(self.tempdir, "live", "example.org", + "README"), 'a'): + pass + config["archive"] = archive_path config.filename = os.path.join(self.tempdir, "renewal", "example.org.conf") config.write() @@ -770,5 +776,95 @@ class RenewableCertTests(BaseRenewableCertTest): storage.RenewableCert(self.config.filename, self.cli_config, update_symlinks=True) +class DeleteFilesTest(BaseRenewableCertTest): + """Tests for certbot.storage.delete_files""" + def setUp(self): + super(DeleteFilesTest, self).setUp() + for kind in ALL_FOUR: + kind_path = os.path.join(self.tempdir, "live", "example.org", + kind + ".pem") + with open(kind_path, 'a'): + pass + self.config.write() + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.renewal_configs_dir, "example.org.conf"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.live_dir, "example.org"))) + self.assertTrue(os.path.exists(os.path.join( + self.tempdir, "archive", "example.org"))) + + def _call(self): + from certbot import storage + with mock.patch("certbot.storage.logger"): + storage.delete_files(self.cli_config, "example.org") + + def test_delete_all_files(self): + self._call() + + self.assertFalse(os.path.exists(os.path.join( + self.cli_config.renewal_configs_dir, "example.org.conf"))) + self.assertFalse(os.path.exists(os.path.join( + self.cli_config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(os.path.join( + self.tempdir, "archive", "example.org"))) + + def test_bad_renewal_config(self): + with open(self.config.filename, 'a') as config_file: + config_file.write("asdfasfasdfasdf") + + self.assertRaises(errors.CertStorageError, self._call) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(os.path.join( + self.cli_config.renewal_configs_dir, "example.org.conf"))) + + def test_no_renewal_config(self): + os.remove(self.config.filename) + self.assertRaises(errors.CertStorageError, self._call) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(self.config.filename)) + + def test_no_cert_file(self): + os.remove(os.path.join( + self.cli_config.live_dir, "example.org", "cert.pem")) + self._call() + self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(os.path.join( + self.cli_config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(os.path.join( + self.tempdir, "archive", "example.org"))) + + def test_no_readme_file(self): + os.remove(os.path.join( + self.cli_config.live_dir, "example.org", "README")) + self._call() + self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(os.path.join( + self.cli_config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(os.path.join( + self.tempdir, "archive", "example.org"))) + + def test_livedir_not_empty(self): + with open(os.path.join( + self.cli_config.live_dir, "example.org", "other_file"), 'a'): + pass + self._call() + self.assertFalse(os.path.exists(self.config.filename)) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(os.path.join( + self.tempdir, "archive", "example.org"))) + + def test_no_archive(self): + archive_dir = os.path.join(self.tempdir, "archive", "example.org") + os.rmdir(archive_dir) + self._call() + self.assertFalse(os.path.exists(self.config.filename)) + self.assertFalse(os.path.exists(os.path.join( + self.cli_config.live_dir, "example.org"))) + self.assertFalse(os.path.exists(archive_dir)) + + if __name__ == "__main__": unittest.main() # pragma: no cover From ae379568b12b28112f6798366002f3265d77217e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 19 Dec 2016 12:45:40 -0800 Subject: [PATCH 300/331] Mitigate problems for people who run without -n (#3916) * CLI flag for forcing interactivity * add --force-interactive * Add force_interactive error checking and tests * Add force_interactive parameter to FileDisplay * add _can_interact * Add _return_default * Add **unused_kwargs to NoninteractiveDisplay * improve _return_default assertion * Change IDisplay calls and write tests * Document force_interactive in interfaces.py * Don't force_interactive with a new prompt * Warn when skipping an interaction for the first time * add specific logger.debug message --- certbot-apache/certbot_apache/configurator.py | 2 +- certbot-apache/certbot_apache/display_ops.py | 6 +- .../certbot_apache/tests/display_ops_test.py | 3 +- certbot-apache/certbot_apache/tests/util.py | 3 +- certbot/cert_manager.py | 5 +- certbot/cli.py | 15 ++ certbot/constants.py | 3 + certbot/display/enhancements.py | 3 +- certbot/display/ops.py | 12 +- certbot/display/util.py | 142 +++++++++++++++--- certbot/interfaces.py | 47 +++++- certbot/main.py | 12 +- certbot/plugins/manual.py | 3 +- certbot/plugins/selection.py | 6 +- certbot/plugins/selection_test.py | 3 +- certbot/plugins/standalone.py | 4 +- certbot/plugins/util.py | 5 +- certbot/plugins/webroot.py | 6 +- certbot/reverter.py | 2 +- certbot/tests/cli_test.py | 6 + certbot/tests/display/ops_test.py | 12 +- certbot/tests/display/util_test.py | 99 +++++++++--- tests/display.py | 2 +- 23 files changed, 320 insertions(+), 81 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 1bb0a1e1a..b200d5eaa 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -463,7 +463,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): zope.component.getUtility(interfaces.IDisplay).notification( "Apache mod_macro seems to be in use in file(s):\n{0}" "\n\nUnfortunately mod_macro is not yet supported".format( - "\n ".join(vhost_macro))) + "\n ".join(vhost_macro)), force_interactive=True) return all_names diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index 527de1001..22aafc0fe 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -85,7 +85,8 @@ def _vhost_menu(domain, vhosts): "or Address of {0}.{1}Which virtual host would you " "like to choose?\n(note: conf files with multiple " "vhosts are not yet supported)".format(domain, os.linesep), - choices, help_label="More Info", ok_label="Select") + choices, help_label="More Info", + ok_label="Select", force_interactive=True) except errors.MissingCommandlineFlag: msg = ("Encountered vhost ambiguity but unable to ask for user guidance in " "non-interactive mode. Currently Certbot needs each vhost to be " @@ -100,4 +101,5 @@ def _vhost_menu(domain, vhosts): def _more_info_vhost(vhost): zope.component.getUtility(interfaces.IDisplay).notification( "Virtual Host Information:{0}{1}{0}{2}".format( - os.linesep, "-" * (display_util.WIDTH - 4), str(vhost))) + os.linesep, "-" * (display_util.WIDTH - 4), str(vhost)), + force_interactive=True) diff --git a/certbot-apache/certbot_apache/tests/display_ops_test.py b/certbot-apache/certbot_apache/tests/display_ops_test.py index 585661c7f..dea1e4433 100644 --- a/certbot-apache/certbot_apache/tests/display_ops_test.py +++ b/certbot-apache/certbot_apache/tests/display_ops_test.py @@ -17,7 +17,8 @@ class SelectVhostTest(unittest.TestCase): """Tests for certbot_apache.display_ops.select_vhost.""" def setUp(self): - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + zope.component.provideUtility(display_util.FileDisplay(sys.stdout, + False)) self.base_dir = "/example_path" self.vhosts = util.get_vh_truth( self.base_dir, "debian_apache_2_4/multiple_vhosts") diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 6a0a83615..3c33a0e19 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -64,7 +64,8 @@ class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods vhost_root="debian_apache_2_4/multiple_vhosts/apache2/sites-available"): super(ParserTest, self).setUp(test_dir, config_root, vhost_root) - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + zope.component.provideUtility(display_util.FileDisplay(sys.stdout, + False)) from certbot_apache.parser import ApacheParser self.aug = augeas.Augeas( diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index d8752554a..35b12e1bb 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -47,8 +47,9 @@ def rename_lineage(config): new_certname = config.new_certname if not new_certname: - code, new_certname = disp.input("Enter the new name for certificate {0}" - .format(certname), flag="--updated-cert-name") + code, new_certname = disp.input( + "Enter the new name for certificate {0}".format(certname), + flag="--updated-cert-name", force_interactive=True) if code != display_util.OK or not new_certname: raise errors.Error("User ended interaction.") diff --git a/certbot/cli.py b/certbot/cli.py index 89c171829..0adb7a4b5 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -521,8 +521,17 @@ class HelpfulArgumentParser(object): # Do any post-parsing homework here if self.verb == "renew": + if parsed_args.force_interactive: + raise errors.Error( + "{0} cannot be used with renew".format( + constants.FORCE_INTERACTIVE_FLAG)) parsed_args.noninteractive_mode = True + if parsed_args.force_interactive and parsed_args.noninteractive_mode: + raise errors.Error( + "Flag for non-interactive mode and {0} conflict".format( + constants.FORCE_INTERACTIVE_FLAG)) + if parsed_args.staging or parsed_args.dry_run: self.set_test_server(parsed_args) @@ -808,6 +817,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " "which ones are required if it finds one missing") + helpful.add( + [None, "register", "run", "certonly"], + constants.FORCE_INTERACTIVE_FLAG, action="store_true", + help="Force Certbot to be interactive even if it detects it's not " + "being run in a terminal. This flag cannot be used with the " + "renew subcommand.") helpful.add( [None, "run", "certonly"], "-d", "--domains", "--domain", dest="domains", diff --git a/certbot/constants.py b/certbot/constants.py index 2e74cfd86..7d713d29f 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -92,3 +92,6 @@ TEMP_CHECKPOINT_DIR = "temp_checkpoint" RENEWAL_CONFIGS_DIR = "renewal" """Renewal configs directory, relative to `IConfig.config_dir`.""" + +FORCE_INTERACTIVE_FLAG = "--force-interactive" +"""Flag to disable TTY checking in IDisplay.""" diff --git a/certbot/display/enhancements.py b/certbot/display/enhancements.py index 3b128a874..d2ffe2e0d 100644 --- a/certbot/display/enhancements.py +++ b/certbot/display/enhancements.py @@ -48,7 +48,8 @@ def redirect_by_default(): code, selection = util(interfaces.IDisplay).menu( "Please choose whether HTTPS access is required or optional.", - choices, default=0, cli_flag="--redirect / --no-redirect") + choices, default=0, + cli_flag="--redirect / --no-redirect", force_interactive=True) if code != display_util.OK: return False diff --git a/certbot/display/ops.py b/certbot/display/ops.py index b9cd1e38c..85343fdc3 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -46,7 +46,8 @@ def get_email(invalid=False, optional=True): while True: try: code, email = z_util(interfaces.IDisplay).input( - invalid_prefix + msg if invalid else msg) + invalid_prefix + msg if invalid else msg, + force_interactive=True) except errors.MissingCommandlineFlag: msg = ("You should register before running non-interactively, " "or provide --agree-tos and --email flags.") @@ -79,7 +80,7 @@ def choose_account(accounts): labels = [acc.slug for acc in accounts] code, index = z_util(interfaces.IDisplay).menu( - "Please choose an account", labels) + "Please choose an account", labels, force_interactive=True) if code == display_util.OK: return accounts[index] else: @@ -157,7 +158,7 @@ def _filter_names(names): code, names = z_util(interfaces.IDisplay).checklist( "Which names would you like to activate HTTPS for?", - tags=sorted_names, cli_flag="--domains") + tags=sorted_names, cli_flag="--domains", force_interactive=True) return code, [str(s) for s in names] @@ -173,7 +174,7 @@ def _choose_names_manually(prompt_prefix=""): code, input_ = z_util(interfaces.IDisplay).input( prompt_prefix + "Please enter in your domain name(s) (comma and/or space separated) ", - cli_flag="--domains") + cli_flag="--domains", force_interactive=True) if code == display_util.OK: invalid_domains = dict() @@ -211,7 +212,8 @@ def _choose_names_manually(prompt_prefix=""): if retry_message: # We had error in input - retry = z_util(interfaces.IDisplay).yesno(retry_message) + retry = z_util(interfaces.IDisplay).yesno(retry_message, + force_interactive=True) if retry: return _choose_names_manually() else: diff --git a/certbot/display/util.py b/certbot/display/util.py index 47bce87b4..0796a0e94 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -1,14 +1,18 @@ """Certbot display.""" +import logging import os import textwrap +import sys import six import zope.interface +from certbot import constants from certbot import interfaces from certbot import errors from certbot.display import completer +logger = logging.getLogger(__name__) WIDTH = 72 @@ -50,19 +54,25 @@ def _wrap_lines(msg): @zope.interface.implementer(interfaces.IDisplay) class FileDisplay(object): """File-based display.""" + # pylint: disable=too-many-arguments + # see https://github.com/certbot/certbot/issues/3915 - def __init__(self, outfile): + def __init__(self, outfile, force_interactive): super(FileDisplay, self).__init__() self.outfile = outfile + self.force_interactive = force_interactive + self.skipped_interaction = False - def notification(self, message, pause=True, wrap=True): - # pylint: disable=unused-argument + def notification(self, message, pause=True, + wrap=True, force_interactive=False): """Displays a notification and waits for user acceptance. :param str message: Message to display :param bool pause: Whether or not the program should pause for the user's confirmation :param bool wrap: Whether or not the application should wrap text + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions """ side_frame = "-" * 79 @@ -72,10 +82,14 @@ class FileDisplay(object): "{line}{frame}{line}{msg}{line}{frame}{line}".format( line=os.linesep, frame=side_frame, msg=message)) if pause: - six.moves.input("Press Enter to Continue") + if self._can_interact(force_interactive): + six.moves.input("Press Enter to Continue") + else: + logger.debug("Not pausing for user confirmation") def menu(self, message, choices, ok_label="", cancel_label="", - help_label="", **unused_kwargs): + help_label="", default=None, + cli_flag=None, force_interactive=False, **unused_kwargs): # pylint: disable=unused-argument """Display a menu. @@ -86,7 +100,10 @@ class FileDisplay(object): :param choices: Menu lines, len must be > 0 :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) - :param dict _kwargs: absorbs default / cli_args + :param default: default value to return (if one exists) + :param str cli_flag: option used to set this value with the CLI + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: tuple of (`code`, `index`) where `code` - str display exit code @@ -95,18 +112,25 @@ class FileDisplay(object): :rtype: tuple """ + if self._return_default(message, default, cli_flag, force_interactive): + return OK, default + self._print_menu(message, choices) code, selection = self._get_valid_int_ans(len(choices)) return code, selection - 1 - def input(self, message, **unused_kwargs): + def input(self, message, default=None, + cli_flag=None, force_interactive=False, **unused_kwargs): # pylint: disable=no-self-use """Accept input from the user. :param str message: message to display to the user - :param dict _kwargs: absorbs default / cli_args + :param default: default value to return (if one exists) + :param str cli_flag: option used to set this value with the CLI + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: tuple of (`code`, `input`) where `code` - str display exit code @@ -114,6 +138,9 @@ class FileDisplay(object): :rtype: tuple """ + if self._return_default(message, default, cli_flag, force_interactive): + return OK, default + ans = six.moves.input( textwrap.fill( "%s (Enter 'c' to cancel): " % message, @@ -126,7 +153,8 @@ class FileDisplay(object): else: return OK, ans - def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): + def yesno(self, message, yes_label="Yes", no_label="No", default=None, + cli_flag=None, force_interactive=False, **unused_kwargs): """Query the user with a yes/no question. Yes and No label must begin with different letters, and must contain at @@ -135,12 +163,18 @@ class FileDisplay(object): :param str message: question for the user :param str yes_label: Label of the "Yes" parameter :param str no_label: Label of the "No" parameter - :param dict _kwargs: absorbs default / cli_args + :param default: default value to return (if one exists) + :param str cli_flag: option used to set this value with the CLI + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: True for "Yes", False for "No" :rtype: bool """ + if self._return_default(message, default, cli_flag, force_interactive): + return default + side_frame = ("-" * 79) + os.linesep message = _wrap_lines(message) @@ -162,14 +196,18 @@ class FileDisplay(object): ans.startswith(no_label[0].upper())): return False - def checklist(self, message, tags, default_status=True, **unused_kwargs): + def checklist(self, message, tags, default_status=True, default=None, + cli_flag=None, force_interactive=False, **unused_kwargs): # pylint: disable=unused-argument """Display a checklist. :param str message: Message to display to user :param list tags: `str` tags to select, len(tags) > 0 :param bool default_status: Not used for FileDisplay - :param dict _kwargs: absorbs default / cli_args + :param default: default value to return (if one exists) + :param str cli_flag: option used to set this value with the CLI + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: tuple of (`code`, `tags`) where `code` - str display exit code @@ -177,6 +215,9 @@ class FileDisplay(object): :rtype: tuple """ + if self._return_default(message, default, cli_flag, force_interactive): + return OK, default + while True: self._print_menu(message, tags) @@ -197,10 +238,65 @@ class FileDisplay(object): else: return code, [] - def directory_select(self, message, **unused_kwargs): + def _return_default(self, prompt, default, cli_flag, force_interactive): + """Should we return the default instead of prompting the user? + + :param str prompt: prompt for the user + :param default: default answer to prompt + :param str cli_flag: command line option for setting an answer + to this question + :param bool force_interactive: if interactivity is forced by the + IDisplay call + + :returns: True if we should return the default without prompting + :rtype: bool + + """ + msg = "Invalid IDisplay call for this prompt:\n{0}".format(prompt) + if cli_flag: + msg += ("\nYou can set an answer to " + "this prompt with the {0} flag".format(cli_flag)) + assert default is not None or force_interactive, msg + + if self._can_interact(force_interactive): + return False + else: + logger.debug( + "Falling back to default %s for the prompt:\n%s", + default, prompt) + return True + + def _can_interact(self, force_interactive): + """Can we safely interact with the user? + + :param bool force_interactive: if interactivity is forced by the + IDisplay call + + :returns: True if the display can interact with the user + :rtype: bool + + """ + if (self.force_interactive or force_interactive or + sys.stdin.isatty() and self.outfile.isatty()): + return True + elif not self.skipped_interaction: + logger.warning( + "Skipped user interaction because Certbot doesn't appear to " + "be running in a terminal. You should probably include " + "--non-interactive or %s on the command line.", + constants.FORCE_INTERACTIVE_FLAG) + self.skipped_interaction = True + return False + + def directory_select(self, message, default=None, cli_flag=None, + force_interactive=False, **unused_kwargs): """Display a directory selection screen. :param str message: prompt to give the user + :param default: default value to return (if one exists) + :param str cli_flag: option used to set this value with the CLI + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: tuple of the form (`code`, `string`) where `code` - display exit code @@ -208,7 +304,7 @@ class FileDisplay(object): """ with completer.Completer(): - return self.input(message) + return self.input(message, default, cli_flag, force_interactive) def _scrub_checklist_input(self, indices, tags): # pylint: disable=no-self-use @@ -310,7 +406,7 @@ class FileDisplay(object): class NoninteractiveDisplay(object): """An iDisplay implementation that never asks for interactive user input""" - def __init__(self, outfile): + def __init__(self, outfile, *unused_args, **unused_kwargs): super(NoninteractiveDisplay, self).__init__() self.outfile = outfile @@ -324,7 +420,7 @@ class NoninteractiveDisplay(object): msg += "\n\n(You can set this with the {0} flag)".format(cli_flag) raise errors.MissingCommandlineFlag(msg) - def notification(self, message, pause=False, wrap=True): + def notification(self, message, pause=False, wrap=True, **unused_kwargs): # pylint: disable=unused-argument """Displays a notification without waiting for user acceptance. @@ -341,7 +437,7 @@ class NoninteractiveDisplay(object): line=os.linesep, frame=side_frame, msg=message)) def menu(self, message, choices, ok_label=None, cancel_label=None, - help_label=None, default=None, cli_flag=None): + help_label=None, default=None, cli_flag=None, *unused_kwargs): # pylint: disable=unused-argument,too-many-arguments """Avoid displaying a menu. @@ -364,7 +460,7 @@ class NoninteractiveDisplay(object): return OK, default - def input(self, message, default=None, cli_flag=None): + def input(self, message, default=None, cli_flag=None, **unused_kwargs): """Accept input from the user. :param str message: message to display to the user @@ -381,7 +477,8 @@ class NoninteractiveDisplay(object): else: return OK, default - def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None): + def yesno(self, message, yes_label=None, no_label=None, + default=None, cli_flag=None, **unused_kwargs): # pylint: disable=unused-argument """Decide Yes or No, without asking anybody @@ -398,8 +495,8 @@ class NoninteractiveDisplay(object): else: return default - def checklist(self, message, tags, default=None, cli_flag=None, **kwargs): - # pylint: disable=unused-argument + def checklist(self, message, tags, default=None, + cli_flag=None, **unused_kwargs): """Display a checklist. :param str message: Message to display to user @@ -417,7 +514,8 @@ class NoninteractiveDisplay(object): else: return OK, default - def directory_select(self, message, default=None, cli_flag=None): + def directory_select(self, message, default=None, + cli_flag=None, **unused_kwargs): """Simulate prompting the user for a directory. This function returns default if it is not ``None``, otherwise, diff --git a/certbot/interfaces.py b/certbot/interfaces.py index f2b3faf21..46b53129b 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -361,21 +361,29 @@ class IInstaller(IPlugin): class IDisplay(zope.interface.Interface): """Generic display.""" + # pylint: disable=too-many-arguments + # see https://github.com/certbot/certbot/issues/3915 - def notification(message, pause, wrap=True): + def notification(message, pause, wrap=True, force_interactive=False): """Displays a string message :param str message: Message to display :param bool pause: Whether or not the application should pause for confirmation (if available) :param bool wrap: Whether or not the application should wrap text + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions """ - def menu(message, choices, ok_label="OK", # pylint: disable=too-many-arguments - cancel_label="Cancel", help_label="", default=None, cli_flag=None): + def menu(message, choices, ok_label="OK", + cancel_label="Cancel", help_label="", + default=None, cli_flag=None, force_interactive=False): """Displays a generic menu. + When not setting force_interactive=True, you must provide a + default value. + :param str message: message to display :param choices: choices @@ -386,6 +394,8 @@ class IDisplay(zope.interface.Interface): :param str help_label: label for Help button :param int default: default (non-interactive) choice from the menu :param str cli_flag: to automate choice from the menu, eg "--keep" + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: tuple of (`code`, `index`) where `code` - str display exit code @@ -396,10 +406,16 @@ class IDisplay(zope.interface.Interface): """ - def input(message, default=None, cli_args=None): + def input(message, default=None, cli_args=None, force_interactive=False): """Accept input from the user. + When not setting force_interactive=True, you must provide a + default value. + :param str message: message to display to the user + :param str default: default (non-interactive) response to prompt + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: tuple of (`code`, `input`) where `code` - str display exit code @@ -412,14 +428,19 @@ class IDisplay(zope.interface.Interface): """ def yesno(message, yes_label="Yes", no_label="No", default=None, - cli_args=None): + cli_args=None, force_interactive=False): """Query the user with a yes/no question. Yes and No label must begin with different letters. + When not setting force_interactive=True, you must provide a + default value. + :param str message: question for the user :param str default: default (non-interactive) choice from the menu :param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect" + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: True for "Yes", False for "No" :rtype: bool @@ -429,14 +450,20 @@ class IDisplay(zope.interface.Interface): """ - def checklist(message, tags, default_state, default=None, cli_args=None): + def checklist(message, tags, default_state, + default=None, cli_args=None, force_interactive=False): """Allow for multiple selections from a menu. + When not setting force_interactive=True, you must provide a + default value. + :param str message: message to display to the user :param list tags: where each is of type :class:`str` len(tags) > 0 :param bool default_status: If True, items are in a selected state by default. :param str default: default (non-interactive) state of the checklist :param str cli_flag: to automate choice from the menu, eg "--domains" + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: tuple of the form (code, list_tags) where `code` - int display exit code @@ -448,15 +475,21 @@ class IDisplay(zope.interface.Interface): """ - def directory_select(self, message, default=None, cli_flag=None): + def directory_select(self, message, default=None, + cli_flag=None, force_interactive=False): """Display a directory selection screen. + When not setting force_interactive=True, you must provide a + default value. + :param str message: prompt to give the user :param default: the default value to return, if one exists, when using the NoninteractiveDisplay :param str cli_flag: option used to set this value with the CLI, if one exists, to be included in error messages given by NoninteractiveDisplay + :param bool force_interactive: True if it's safe to prompt the user + because it won't cause any workflow regressions :returns: tuple of the form (`code`, `string`) where `code` - int display exit code diff --git a/certbot/main.py b/certbot/main.py index 6a1653193..c0e2f5271 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -139,7 +139,8 @@ def _handle_subset_cert_request(config, domains, cert): br=os.linesep) if config.expand or config.renew_by_default or zope.component.getUtility( interfaces.IDisplay).yesno(question, "Expand", "Cancel", - cli_flag="--expand"): + cli_flag="--expand", + force_interactive=True): return "renew", cert else: reporter_util = zope.component.getUtility(interfaces.IReporter) @@ -188,7 +189,8 @@ def _handle_identical_cert_request(config, lineage): "Renew & replace the cert (limit ~5 per 7 days)"] display = zope.component.getUtility(interfaces.IDisplay) - response = display.menu(question, choices, "OK", "Cancel", default=0) + response = display.menu(question, choices, "OK", "Cancel", + default=0, force_interactive=True) if response[0] == display_util.CANCEL: # TODO: Add notification related to command-line options for # skipping the menu for this case. @@ -365,7 +367,8 @@ def _determine_account(config): "server at {1}".format( regr.terms_of_service, config.server)) obj = zope.component.getUtility(interfaces.IDisplay) - return obj.yesno(msg, "Agree", "Cancel", cli_flag="--agree-tos") + return obj.yesno(msg, "Agree", "Cancel", + cli_flag="--agree-tos", force_interactive=True) try: acc, acme = client.register( @@ -788,7 +791,8 @@ def set_displayer(config): elif config.noninteractive_mode: displayer = display_util.NoninteractiveDisplay(sys.stdout) else: - displayer = display_util.FileDisplay(sys.stdout) + displayer = display_util.FileDisplay(sys.stdout, + config.force_interactive) zope.component.provideUtility(displayer) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 5f933f8bc..646b1d340 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -251,7 +251,8 @@ s.serve_forever()" """ if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")): if not zope.component.getUtility(interfaces.IDisplay).yesno( self.IP_DISCLAIMER, "Yes", "No", - cli_flag="--manual-public-ip-logging-ok"): + cli_flag="--manual-public-ip-logging-ok", + force_interactive=True): raise errors.PluginError("Must agree to IP logging to proceed") else: self.config.namespace.manual_public_ip_logging_ok = True diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index ed0991a89..16932232a 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -111,7 +111,8 @@ def choose_plugin(prepared, question): while True: disp = z_util(interfaces.IDisplay) - code, index = disp.menu(question, opts, help_label="More Info") + code, index = disp.menu( + question, opts, help_label="More Info", force_interactive=True) if code == display_util.OK: plugin_ep = prepared[index] @@ -127,7 +128,8 @@ def choose_plugin(prepared, question): msg = "Reported Error: %s" % prepared[index].prepare() else: msg = prepared[index].init().more_info() - z_util(interfaces.IDisplay).notification(msg) + z_util(interfaces.IDisplay).notification(msg, + force_interactive=True) else: return None diff --git a/certbot/plugins/selection_test.py b/certbot/plugins/selection_test.py index 001ca5cff..c0494e565 100644 --- a/certbot/plugins/selection_test.py +++ b/certbot/plugins/selection_test.py @@ -110,7 +110,8 @@ class ChoosePluginTest(unittest.TestCase): """Tests for certbot.plugins.selection.choose_plugin.""" def setUp(self): - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + zope.component.provideUtility(display_util.FileDisplay(sys.stdout, + False)) self.mock_apache = mock.Mock( description_with_name="a", misconfigured=True) self.mock_stand = mock.Mock( diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index e8c11a416..4fc52479f 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -243,13 +243,13 @@ class Authenticator(common.Plugin): "Could not bind TCP port {0} because you don't have " "the appropriate permissions (for example, you " "aren't running this program as " - "root).".format(error.port)) + "root).".format(error.port), force_interactive=True) elif error.socket_error.errno == socket.errno.EADDRINUSE: display.notification( "Could not bind TCP port {0} because it is already in " "use by another process on this system (such as a web " "server). Please stop the program in question and then " - "try again.".format(error.port)) + "try again.".format(error.port), force_interactive=True) else: raise # XXX: How to handle unknown errors in binding? diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index 8f6a62a7f..1964ae349 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -103,7 +103,7 @@ def already_listening_socket(port, renewer=False): "Port {0} is already in use by another process. This will " "prevent us from binding to that port. Please stop the " "process that is populating the port in question and try " - "again. {1}".format(port, extra)) + "again. {1}".format(port, extra), force_interactive=True) return True finally: testsocket.close() @@ -151,7 +151,8 @@ def already_listening_psutil(port, renewer=False): "The program {0} (process ID {1}) is already listening " "on TCP port {2}. This will prevent us from binding to " "that port. Please stop the {0} program temporarily " - "and then try again.{3}".format(name, pid, port, extra)) + "and then try again.{3}".format(name, pid, port, extra), + force_interactive=True) return True except (psutil.NoSuchProcess, psutil.AccessDenied): # Perhaps the result of a race where the process could have diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index e9c3bcdda..09671f989 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -129,7 +129,8 @@ to serve all files under specified web root ({0}).""" "public_html or webroot directory. The webroot " "plugin works by temporarily saving necessary " "resources in the HTTP server's webroot directory " - "to pass domain validation challenges.") + "to pass domain validation challenges.", + force_interactive=True) else: # code == display_util.OK return None if index == 0 else known_webroots[index - 1] @@ -138,7 +139,8 @@ to serve all files under specified web root ({0}).""" while True: code, webroot = display.directory_select( - "Input the webroot for {0}:".format(domain)) + "Input the webroot for {0}:".format(domain), + force_interactive=True) if code == display_util.HELP: # Displaying help is not currently implemented return None diff --git a/certbot/reverter.py b/certbot/reverter.py index 714a38b8b..32355782e 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -181,7 +181,7 @@ class Reverter(object): if for_logging: return os.linesep.join(output) zope.component.getUtility(interfaces.IDisplay).notification( - os.linesep.join(output)) + os.linesep.join(output), force_interactive=True) def add_to_temp_checkpoint(self, save_files, save_notes): """Add files to temporary checkpoint. diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 2755d992c..e3bd28a5e 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -262,6 +262,12 @@ class ParseTest(unittest.TestCase): self.assertFalse(cli.option_was_set( config_dir_option, cli.flag_default(config_dir_option))) + def test_force_interactive(self): + self.assertRaises( + errors.Error, self.parse, "renew --force-interactive".split()) + self.assertRaises( + errors.Error, self.parse, "-n --force-interactive".split()) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 1b535bf3a..e2735cbde 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -84,7 +84,8 @@ class GetEmailTest(unittest.TestCase): class ChooseAccountTest(unittest.TestCase): """Tests for certbot.display.ops.choose_account.""" def setUp(self): - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + zope.component.provideUtility(display_util.FileDisplay(sys.stdout, + False)) self.accounts_dir = tempfile.mkdtemp("accounts") self.account_keys_dir = os.path.join(self.accounts_dir, "keys") @@ -127,7 +128,8 @@ class ChooseAccountTest(unittest.TestCase): class GenSSLLabURLs(unittest.TestCase): """Loose test of _gen_ssl_lab_urls. URL can change easily in the future.""" def setUp(self): - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + zope.component.provideUtility(display_util.FileDisplay(sys.stdout, + False)) @classmethod def _call(cls, domains): @@ -146,7 +148,8 @@ class GenSSLLabURLs(unittest.TestCase): class GenHttpsNamesTest(unittest.TestCase): """Test _gen_https_names.""" def setUp(self): - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + zope.component.provideUtility(display_util.FileDisplay(sys.stdout, + False)) @classmethod def _call(cls, domains): @@ -193,7 +196,8 @@ class GenHttpsNamesTest(unittest.TestCase): class ChooseNamesTest(unittest.TestCase): """Test choose names.""" def setUp(self): - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + zope.component.provideUtility(display_util.FileDisplay(sys.stdout, + False)) self.mock_install = mock.MagicMock() @classmethod diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index fa1cb89ba..51fbc2828 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -20,10 +20,11 @@ class FileOutputDisplayTest(unittest.TestCase): functions look to a user, uncomment the test_visual function. """ + # pylint:disable=too-many-public-methods def setUp(self): super(FileOutputDisplayTest, self).setUp() self.mock_stdout = mock.MagicMock() - self.displayer = display_util.FileDisplay(self.mock_stdout) + self.displayer = display_util.FileDisplay(self.mock_stdout, False) def test_notification_no_pause(self): self.displayer.notification("message", False) @@ -33,56 +34,84 @@ class FileOutputDisplayTest(unittest.TestCase): def test_notification_pause(self): with mock.patch("six.moves.input", return_value="enter"): - self.displayer.notification("message") + self.displayer.notification("message", force_interactive=True) self.assertTrue("message" in self.mock_stdout.write.call_args[0][0]) + def test_notification_noninteractive(self): + self._force_noninteractive(self.displayer.notification, "message") + string = self.mock_stdout.write.call_args[0][0] + self.assertTrue("message" in string) + @mock.patch("certbot.display.util." "FileDisplay._get_valid_int_ans") def test_menu(self, mock_ans): mock_ans.return_value = (display_util.OK, 1) - ret = self.displayer.menu("message", CHOICES) + ret = self.displayer.menu("message", CHOICES, force_interactive=True) self.assertEqual(ret, (display_util.OK, 0)) def test_input_cancel(self): with mock.patch("six.moves.input", return_value="c"): - code, _ = self.displayer.input("message") + code, _ = self.displayer.input("message", force_interactive=True) self.assertTrue(code, display_util.CANCEL) def test_input_normal(self): with mock.patch("six.moves.input", return_value="domain.com"): - code, input_ = self.displayer.input("message") + code, input_ = self.displayer.input("message", force_interactive=True) self.assertEqual(code, display_util.OK) self.assertEqual(input_, "domain.com") + def test_input_noninteractive(self): + default = "foo" + code, input_ = self._force_noninteractive( + self.displayer.input, "message", default=default) + + self.assertEqual(code, display_util.OK) + self.assertEqual(input_, default) + + def test_input_assertion_fail(self): + self.assertRaises(AssertionError, self._force_noninteractive, + self.displayer.input, "message", cli_flag="--flag") + def test_yesno(self): with mock.patch("six.moves.input", return_value="Yes"): - self.assertTrue(self.displayer.yesno("message")) + self.assertTrue(self.displayer.yesno( + "message", force_interactive=True)) with mock.patch("six.moves.input", return_value="y"): - self.assertTrue(self.displayer.yesno("message")) + self.assertTrue(self.displayer.yesno( + "message", force_interactive=True)) with mock.patch("six.moves.input", side_effect=["maybe", "y"]): - self.assertTrue(self.displayer.yesno("message")) + self.assertTrue(self.displayer.yesno( + "message", force_interactive=True)) with mock.patch("six.moves.input", return_value="No"): - self.assertFalse(self.displayer.yesno("message")) + self.assertFalse(self.displayer.yesno( + "message", force_interactive=True)) with mock.patch("six.moves.input", side_effect=["cancel", "n"]): - self.assertFalse(self.displayer.yesno("message")) + self.assertFalse(self.displayer.yesno( + "message", force_interactive=True)) with mock.patch("six.moves.input", return_value="a"): - self.assertTrue(self.displayer.yesno("msg", yes_label="Agree")) + self.assertTrue(self.displayer.yesno( + "msg", yes_label="Agree", force_interactive=True)) + + def test_yesno_noninteractive(self): + self.assertTrue(self._force_noninteractive( + self.displayer.yesno, "message", default=True)) @mock.patch("certbot.display.util.FileDisplay.input") def test_checklist_valid(self, mock_input): mock_input.return_value = (display_util.OK, "2 1") - code, tag_list = self.displayer.checklist("msg", TAGS) + code, tag_list = self.displayer.checklist( + "msg", TAGS, force_interactive=True) self.assertEqual( (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2"]))) @mock.patch("certbot.display.util.FileDisplay.input") def test_checklist_empty(self, mock_input): mock_input.return_value = (display_util.OK, "") - code, tag_list = self.displayer.checklist("msg", TAGS) + code, tag_list = self.displayer.checklist("msg", TAGS, force_interactive=True) self.assertEqual( (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2", "tag3"]))) @@ -94,7 +123,7 @@ class FileOutputDisplayTest(unittest.TestCase): (display_util.OK, "1") ] - ret = self.displayer.checklist("msg", TAGS) + ret = self.displayer.checklist("msg", TAGS, force_interactive=True) self.assertEqual(ret, (display_util.OK, ["tag1"])) @mock.patch("certbot.display.util.FileDisplay.input") @@ -103,9 +132,17 @@ class FileOutputDisplayTest(unittest.TestCase): (display_util.OK, "10"), (display_util.CANCEL, "1") ] - ret = self.displayer.checklist("msg", TAGS) + ret = self.displayer.checklist("msg", TAGS, force_interactive=True) self.assertEqual(ret, (display_util.CANCEL, [])) + def test_checklist_noninteractive(self): + default = TAGS + code, input_ = self._force_noninteractive( + self.displayer.checklist, "msg", TAGS, default=default) + + self.assertEqual(code, display_util.OK) + self.assertEqual(input_, default) + def test_scrub_checklist_input_valid(self): # pylint: disable=protected-access indices = [ @@ -125,12 +162,36 @@ class FileOutputDisplayTest(unittest.TestCase): @mock.patch("certbot.display.util.FileDisplay.input") def test_directory_select(self, mock_input): - message = "msg" - result = (display_util.OK, "/var/www/html",) + # pylint: disable=star-args + args = ["msg", "/var/www/html", "--flag", True] + result = (display_util.OK, "/var/www/html") mock_input.return_value = result - self.assertEqual(self.displayer.directory_select(message), result) - mock_input.assert_called_once_with(message) + self.assertEqual(self.displayer.directory_select(*args), result) + mock_input.assert_called_once_with(*args) + + def test_directory_select_noninteractive(self): + default = "/var/www/html" + code, input_ = self._force_noninteractive( + self.displayer.directory_select, "msg", default=default) + + self.assertEqual(code, display_util.OK) + self.assertEqual(input_, default) + + def _force_noninteractive(self, func, *args, **kwargs): + skipped_interaction = self.displayer.skipped_interaction + + with mock.patch("certbot.display.util.sys.stdin") as mock_stdin: + mock_stdin.isatty.return_value = False + with mock.patch("certbot.display.util.logger") as mock_logger: + result = func(*args, **kwargs) + + if skipped_interaction: + self.assertFalse(mock_logger.warning.called) + else: + self.assertEqual(mock_logger.warning.call_count, 1) + + return result def test_scrub_checklist_input_invalid(self): # pylint: disable=protected-access diff --git a/tests/display.py b/tests/display.py index 7400788a3..1f548e33d 100644 --- a/tests/display.py +++ b/tests/display.py @@ -18,5 +18,5 @@ def test_visual(displayer, choices): if __name__ == "__main__": - displayer = util.FileDisplay(sys.stdout): + displayer = util.FileDisplay(sys.stdout, False) test_visual(displayer, util_test.CHOICES) From acc501d3a118e84035e7b3a9fed531ca304f2b9b Mon Sep 17 00:00:00 2001 From: Lior Sabag Date: Mon, 19 Dec 2016 22:49:27 +0200 Subject: [PATCH 301/331] Fix typo (#3932) --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 7764408bf..0aeed58b9 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -233,7 +233,7 @@ certificate that contains all of the old domains and one or more additional new domains. ``--allow-subset-of-names`` tells Certbot to continue with cert generation if -only some of the specified domain authorazations can be obtained. This may +only some of the specified domain authorizations can be obtained. This may be useful if some domains specified in a certificate no longer point at this system. From 6a933f1de36233b23dec5f7ca20e736288b6b8d4 Mon Sep 17 00:00:00 2001 From: Craig Smith Date: Tue, 20 Dec 2016 12:32:05 +1030 Subject: [PATCH 302/331] Changed plugin interface return types (#3748). (#3780) --- certbot/interfaces.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 46b53129b..2df2abfe8 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -138,15 +138,15 @@ class IAuthenticator(IPlugin): """ def get_chall_pref(domain): - """Return list of challenge preferences. + """Return `collections.Iterable` of challenge preferences. :param str domain: Domain for which challenge preferences are sought. - :returns: List of challenge types (subclasses of + :returns: `collections.Iterable` of challenge types (subclasses of :class:`acme.challenges.Challenge`) with the most preferred challenges first. If a type is not specified, it means the Authenticator cannot perform the challenge. - :rtype: list + :rtype: `collections.Iterable` """ @@ -158,7 +158,7 @@ class IAuthenticator(IPlugin): instances, such that it contains types found within :func:`get_chall_pref` only. - :returns: List of ACME + :returns: `collections.Iterable` of ACME :class:`~acme.challenges.ChallengeResponse` instances or if the :class:`~acme.challenges.Challenge` cannot be fulfilled then: @@ -168,7 +168,7 @@ class IAuthenticator(IPlugin): ``False`` Authenticator will never be able to perform (error). - :rtype: :class:`list` of + :rtype: :class:`collections.Iterable` of :class:`acme.challenges.ChallengeResponse`, where responses are required to be returned in the same order as corresponding input challenges @@ -254,7 +254,7 @@ class IInstaller(IPlugin): def get_all_names(): """Returns all names that may be authenticated. - :rtype: `list` of `str` + :rtype: `collections.Iterable` of `str` """ @@ -289,11 +289,11 @@ class IInstaller(IPlugin): """ def supported_enhancements(): - """Returns a list of supported enhancements. + """Returns a `collections.Iterable` of supported enhancements. :returns: supported enhancements which should be a subset of :const:`~certbot.constants.ENHANCEMENTS` - :rtype: :class:`list` of :class:`str` + :rtype: :class:`collections.Iterable` of :class:`str` """ From f92254769bbf60c3777a2abe337c470a9edb7d5f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 20 Dec 2016 14:34:12 -0800 Subject: [PATCH 303/331] I promise checklists are OK (fixes #3934) (#3940) * TIL checklist calls input * full coverage on certbot/display/util.py * improve no double warning test --- certbot/display/util.py | 3 +- certbot/tests/display/util_test.py | 53 +++++++++++++++++++----------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 0796a0e94..ebfce78e2 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -223,7 +223,8 @@ class FileDisplay(object): code, ans = self.input("Select the appropriate numbers separated " "by commas and/or spaces, or leave input " - "blank to select all options shown") + "blank to select all options shown", + force_interactive=True) if code == OK: if len(ans.strip()) == 0: diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index 51fbc2828..7b6bd842e 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -43,6 +43,19 @@ class FileOutputDisplayTest(unittest.TestCase): string = self.mock_stdout.write.call_args[0][0] self.assertTrue("message" in string) + def test_notification_noninteractive2(self): + # The main purpose of this test is to make sure we only call + # logger.warning once which _force_noninteractive checks internally + self._force_noninteractive(self.displayer.notification, "message") + string = self.mock_stdout.write.call_args[0][0] + self.assertTrue("message" in string) + + self.assertTrue(self.displayer.skipped_interaction) + + self._force_noninteractive(self.displayer.notification, "message2") + string = self.mock_stdout.write.call_args[0][0] + self.assertTrue("message2" in string) + @mock.patch("certbot.display.util." "FileDisplay._get_valid_int_ans") def test_menu(self, mock_ans): @@ -50,6 +63,12 @@ class FileOutputDisplayTest(unittest.TestCase): ret = self.displayer.menu("message", CHOICES, force_interactive=True) self.assertEqual(ret, (display_util.OK, 0)) + def test_menu_noninteractive(self): + default = 0 + result = self._force_noninteractive( + self.displayer.menu, "msg", CHOICES, default=default) + self.assertEqual(result, (display_util.OK, default)) + def test_input_cancel(self): with mock.patch("six.moves.input", return_value="c"): code, _ = self.displayer.input("message", force_interactive=True) @@ -100,38 +119,32 @@ class FileOutputDisplayTest(unittest.TestCase): self.assertTrue(self._force_noninteractive( self.displayer.yesno, "message", default=True)) - @mock.patch("certbot.display.util.FileDisplay.input") + @mock.patch("certbot.display.util.six.moves.input") def test_checklist_valid(self, mock_input): - mock_input.return_value = (display_util.OK, "2 1") + mock_input.return_value = "2 1" code, tag_list = self.displayer.checklist( "msg", TAGS, force_interactive=True) self.assertEqual( (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2"]))) - @mock.patch("certbot.display.util.FileDisplay.input") + @mock.patch("certbot.display.util.six.moves.input") def test_checklist_empty(self, mock_input): - mock_input.return_value = (display_util.OK, "") + mock_input.return_value = "" code, tag_list = self.displayer.checklist("msg", TAGS, force_interactive=True) self.assertEqual( (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2", "tag3"]))) - @mock.patch("certbot.display.util.FileDisplay.input") + @mock.patch("certbot.display.util.six.moves.input") def test_checklist_miss_valid(self, mock_input): - mock_input.side_effect = [ - (display_util.OK, "10"), - (display_util.OK, "tag1 please"), - (display_util.OK, "1") - ] + mock_input.side_effect = ["10", "tag1 please", "1"] ret = self.displayer.checklist("msg", TAGS, force_interactive=True) self.assertEqual(ret, (display_util.OK, ["tag1"])) - @mock.patch("certbot.display.util.FileDisplay.input") + @mock.patch("certbot.display.util.six.moves.input") def test_checklist_miss_quit(self, mock_input): - mock_input.side_effect = [ - (display_util.OK, "10"), - (display_util.CANCEL, "1") - ] + mock_input.side_effect = ["10", "c"] + ret = self.displayer.checklist("msg", TAGS, force_interactive=True) self.assertEqual(ret, (display_util.CANCEL, [])) @@ -160,15 +173,15 @@ class FileOutputDisplayTest(unittest.TestCase): self.displayer._scrub_checklist_input(list_, TAGS)) self.assertEqual(set_tags, exp[i]) - @mock.patch("certbot.display.util.FileDisplay.input") + @mock.patch("certbot.display.util.six.moves.input") def test_directory_select(self, mock_input): # pylint: disable=star-args args = ["msg", "/var/www/html", "--flag", True] - result = (display_util.OK, "/var/www/html") - mock_input.return_value = result + user_input = "/var/www/html" + mock_input.return_value = user_input - self.assertEqual(self.displayer.directory_select(*args), result) - mock_input.assert_called_once_with(*args) + returned = self.displayer.directory_select(*args) + self.assertEqual(returned, (display_util.OK, user_input)) def test_directory_select_noninteractive(self): default = "/var/www/html" From 28ce10fef5d88fe662bff05f986903685bc16cb1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 20 Dec 2016 15:53:52 -0800 Subject: [PATCH 304/331] Don't add ServerAlias directives when the domain is already covered by a wildcard (#3917) * correctly match * and ? in ServerAlias directives * update Apache wildcard test * Consolidate wildcard matching and remove bad test * Test Apache vhost selection with wildcards * Added few more tests to proof vhost selection --- certbot-apache/certbot_apache/configurator.py | 49 ++++++++++++++----- .../certbot_apache/tests/configurator_test.py | 42 +++++++++++++--- .../sites-available/another_wildcard.conf | 11 +++++ .../apache2/sites-available/wildcard.conf | 11 +++++ 4 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/another_wildcard.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index b200d5eaa..27e214362 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -1,6 +1,7 @@ """Apache Configuration based off of Augeas Configurator.""" # pylint: disable=too-many-lines import filecmp +import fnmatch import logging import os import re @@ -362,18 +363,24 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return vhost def included_in_wildcard(self, names, target_name): - """Helper function to see if alias is covered by wildcard""" - target_name = target_name.split(".")[::-1] - wildcards = [domain.split(".")[1:] for domain in - names if domain.startswith("*")] - for wildcard in wildcards: - if len(wildcard) > len(target_name): - continue - for idx, segment in enumerate(wildcard[::-1]): - if segment != target_name[idx]: - break - else: - # https://docs.python.org/2/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops + """Is target_name covered by a wildcard? + + :param names: server aliases + :type names: `collections.Iterable` of `str` + :param str target_name: name to compare with wildcards + + :returns: True if target_name is covered by a wildcard, + otherwise, False + :rtype: bool + + """ + # use lowercase strings because fnmatch can be case sensitive + target_name = target_name.lower() + for name in names: + name = name.lower() + # fnmatch treats "[seq]" specially and [ or ] characters aren't + # valid in Apache but Apache doesn't error out if they are present + if "[" not in name and fnmatch.fnmatch(target_name, name): return True return False @@ -1012,6 +1019,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.find_dir("ServerAlias", target_name, start=vh_path, exclude=False)): return + if self._has_matching_wildcard(vh_path, target_name): + return if not self.parser.find_dir("ServerName", None, start=vh_path, exclude=False): self.parser.add_dir(vh_path, "ServerName", target_name) @@ -1019,6 +1028,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.add_dir(vh_path, "ServerAlias", target_name) self._add_servernames(vhost) + def _has_matching_wildcard(self, vh_path, target_name): + """Is target_name already included in a wildcard in the vhost? + + :param str vh_path: Augeas path to the vhost + :param str target_name: name to compare with wildcards + + :returns: True if there is a wildcard covering target_name in + the vhost in vhost_path, otherwise, False + :rtype: bool + + """ + matches = self.parser.find_dir( + "ServerAlias", start=vh_path, exclude=False) + aliases = (self.aug.get(match) for match in matches) + return self.included_in_wildcard(aliases, target_name) + def _add_name_vhost_if_necessary(self, vhost): """Add NameVirtualHost Directives if necessary for new vhost. diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 2f1d01315..1af425824 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -220,10 +220,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.PluginError, self.config.choose_vhost, "none.com") - def test_choosevhost_select_vhost_with_wildcard(self): - chosen_vhost = self.config.choose_vhost("blue.purple.com", temp=True) - self.assertEqual(self.vh_truth[6], chosen_vhost) - def test_findbest_continues_on_short_domain(self): # pylint: disable=protected-access chosen_vhost = self.config._find_best_vhost("purple.com") @@ -1255,8 +1251,6 @@ class AugeasVhostsTest(util.ApacheTest): self.config = util.get_apache_configurator( self.config_path, self.vhost_path, self.config_dir, self.work_dir) - self.vh_truth = util.get_vh_truth( - self.temp_dir, "debian_apache_2_4/augeas_vhosts") def tearDown(self): shutil.rmtree(self.temp_dir) @@ -1281,5 +1275,41 @@ class AugeasVhostsTest(util.ApacheTest): vhs = self.config.get_virtual_hosts() self.assertEqual([], vhs) + def test_choose_vhost_with_matching_wildcard(self): + names = ( + "an.example.net", "another.example.net", "an.other.example.net") + for name in names: + self.assertFalse(name in self.config.choose_vhost(name).aliases) + + def test_choose_vhost_without_matching_wildcard(self): + mock_path = "certbot_apache.display_ops.select_vhost" + with mock.patch(mock_path, lambda _, vhosts: vhosts[0]): + for name in ("a.example.net", "other.example.net"): + self.assertTrue(name in self.config.choose_vhost(name).aliases) + + def test_choose_vhost_wildcard_not_found(self): + mock_path = "certbot_apache.display_ops.select_vhost" + names = ( + "abc.example.net", "not.there.tld", "aa.wildcard.tld" + ) + with mock.patch(mock_path) as mock_select: + mock_select.return_value = self.config.vhosts[0] + for name in names: + orig_cc = mock_select.call_count + self.config.choose_vhost(name) + self.assertEqual(mock_select.call_count - orig_cc, 1) + + def test_choose_vhost_wildcard_found(self): + mock_path = "certbot_apache.display_ops.select_vhost" + names = ( + "ab.example.net", "a.wildcard.tld", "yetanother.example.net" + ) + with mock.patch(mock_path) as mock_select: + mock_select.return_value = self.config.vhosts[0] + for name in names: + self.config.choose_vhost(name) + self.assertEqual(mock_select.call_count, 0) + + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/another_wildcard.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/another_wildcard.conf new file mode 100644 index 000000000..1a5b7de47 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/another_wildcard.conf @@ -0,0 +1,11 @@ + + ServerName wildcard.tld + ServerAlias ?.wildcard.tld + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf new file mode 100644 index 000000000..b8046e6c9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf @@ -0,0 +1,11 @@ + + ServerName example.net + ServerAlias ??.example.net *.other.example.net *another.example.net + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet From 00e143d369dbf5a14497009a2cda37a120c8ac6d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 20 Dec 2016 16:24:33 -0800 Subject: [PATCH 305/331] Serialize coverage tests (#3919) * Serialize coverage tests * add py27_install env * Separate cover from integration tests * Add docker to py27 integration tests --- .travis.yml | 14 ++++++++------ tox.cover.sh | 5 ++--- tox.ini | 5 +++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index a42e41352..3a9a994a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,12 +17,7 @@ env: matrix: include: - python: "2.7" - env: TOXENV=cover BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - sudo cat /var/log/mysql/error.log - - ps aux | grep mysql - services: docker + env: TOXENV=cover - python: "2.7" env: TOXENV=lint - python: "2.7" @@ -46,6 +41,13 @@ matrix: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql services: docker + - python: "2.7" + env: TOXENV=py27_install BOULDER_INTEGRATION=1 + sudo: required + after_failure: + - sudo cat /var/log/mysql/error.log + - ps aux | grep mysql + services: docker - sudo: required env: TOXENV=apache_compat services: docker diff --git a/tox.cover.sh b/tox.cover.sh index d138a98e5..7243c4708 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -36,9 +36,8 @@ cover () { # specific package, positional argument scopes tests only to # specific package directory; --cover-tests makes sure every tests # is run (c.f. #403) - nosetests -c /dev/null --with-cover --cover-tests --cover-package \ - "$1" --cover-min-percentage="$min" "$1" --processes=-1 \ - --process-timeout=100 + nosetests -c /dev/null --with-cover --cover-tests --cover-package \ + "$1" --cover-min-percentage="$min" "$1" } rm -f .coverage # --cover-erase is off, make sure stats are correct diff --git a/tox.ini b/tox.ini index 1d82092f6..959f44a8d 100644 --- a/tox.ini +++ b/tox.ini @@ -63,6 +63,11 @@ commands = pip install -e .[dev] nosetests -v certbot --processes=-1 --process-timeout=100 +[testenv:py27_install] +basepython = python2.7 +commands = + pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot + [testenv:cover] basepython = python2.7 commands = From 8ebca1c0526962058c17744f3564ee4149ea92f6 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Tue, 20 Dec 2016 17:17:01 -0800 Subject: [PATCH 306/331] Return domains for _find_domains_or_certname (#3937) * Return domains for _find_domains_or_certname * Revamp find_domains_or_certname --- certbot/main.py | 15 ++++++++++++--- certbot/tests/main_test.py | 18 +++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index c0e2f5271..838416822 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -284,17 +284,26 @@ def _find_domains_or_certname(config, installer): """Retrieve domains and certname from config or user input. """ domains = None + certname = config.certname + # first, try to get domains from the config if config.domains: domains = config.domains - elif not config.certname: + # if we can't do that but we have a certname, get the domains + # with that certname + elif certname: + domains = cert_manager.domains_for_certname(config, certname) + + # that certname might not have existed, or there was a problem. + # try to get domains from the user. + if not domains: domains = display_ops.choose_names(installer) - if not domains and not config.certname: + if not domains and not certname: raise errors.Error("Please specify --domains, or --installer that " "will help in domain names autodiscovery, or " "--cert-name for an existing certificate name.") - return domains, config.certname + return domains, certname def _report_new_cert(config, cert_path, fullchain_path): diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 3665f09bb..673952b4e 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -159,10 +159,12 @@ class ObtainCertTest(unittest.TestCase): self.assertRaises(errors.ConfigurationError, self._call, ('certonly --webroot -d example.com -d test.com --cert-name example.com').split()) + @mock.patch('certbot.cert_manager.domains_for_certname') + @mock.patch('certbot.display.ops.choose_names') @mock.patch('certbot.cert_manager.lineage_for_certname') @mock.patch('certbot.main._report_new_cert') def test_find_lineage_for_domains_new_certname(self, mock_report_cert, - mock_lineage): + mock_lineage, mock_choose_names, mock_domains_for_certname): mock_lineage.return_value = None # no lineage with this name but we specified domains so create a new cert @@ -172,8 +174,10 @@ class ObtainCertTest(unittest.TestCase): self.assertTrue(mock_report_cert.call_count == 1) # no lineage with this name and we didn't give domains - self.assertRaises(errors.ConfigurationError, self._call, - ('certonly --webroot --cert-name example.com').split()) + mock_choose_names.return_value = ["somename"] + mock_domains_for_certname.return_value = None + self._call(('certonly --webroot --cert-name example.com').split()) + self.assertTrue(mock_choose_names.called) class FindDomainsOrCertnameTest(unittest.TestCase): """Tests for certbot.main._find_domains_or_certname.""" @@ -193,6 +197,14 @@ class FindDomainsOrCertnameTest(unittest.TestCase): # pylint: disable=protected-access self.assertRaises(errors.Error, main._find_domains_or_certname, mock_config, None) + @mock.patch('certbot.cert_manager.domains_for_certname') + def test_grab_domains(self, mock_domains): + mock_config = mock.Mock(domains=None, certname="one.com") + mock_domains.return_value = ["one.com", "two.com"] + # pylint: disable=protected-access + self.assertEqual(main._find_domains_or_certname(mock_config, None), + (["one.com", "two.com"], "one.com")) + class RevokeTest(unittest.TestCase): """Tests for certbot.main.revoke.""" From 44d5886429c37c0dbb76a8ed60e99773e1c75135 Mon Sep 17 00:00:00 2001 From: Tan Jay Jun Date: Thu, 22 Dec 2016 06:21:52 +0800 Subject: [PATCH 307/331] Add missing comma to documentation for 'renew' subcommand (#3945) --- docs/cli-help.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli-help.txt b/docs/cli-help.txt index cf93daa0e..279b65219 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -1,4 +1,4 @@ -usage: +usage: certbot [SUBCOMMAND] [options] [-d domain] [-d domain] ... Certbot can obtain and install HTTPS/TLS/SSL certificates. By default, @@ -178,7 +178,7 @@ renew: The 'renew' subcommand will attempt to renew all certificates (or more precisely, certificate lineages) you have previously obtained if they are close to expiry, and print a summary of the results. By default, 'renew' - will reuse the options used to create obtain or most recently successfully + will reuse the options used to create, obtain or most recently successfully renew each certificate lineage. You can try it with `--dry-run` first. For more fine-grained control, you can renew individual lineages with the `certonly` subcommand. Hooks are available to run commands before and From 15d2a0ffde27f9563ca272d78629b7d23e99d685 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 21 Dec 2016 14:36:51 -0800 Subject: [PATCH 308/331] Import OCSP code from the historical cert_manager branch (This is pde committing jdkasten's code) --- certbot/ocsp.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 certbot/ocsp.py diff --git a/certbot/ocsp.py b/certbot/ocsp.py new file mode 100644 index 000000000..d7fc06e0d --- /dev/null +++ b/certbot/ocsp.py @@ -0,0 +1,59 @@ +import logging + +from letsencrypt import errors +from letsencrypt import le_util + + +logger = logging.getLogger(__name__) + + +REV_LABEL = "**Revoked**" +EXP_LABEL = "**Expired**" + +def revoked_status(cert_path, chain_path): + """Get revoked status for a particular cert version. + + .. todo:: Make this a non-blocking call + + :param str cert_path: Path to certificate + :param str chain_path: Path to chain certificate + + """ + url, _ = le_util.run_script( + ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"]) + + url = url.rstrip() + host = url.partition("://")[2].rstrip("/") + if not host: + raise errors.Error( + "Unable to get OCSP host from cert, url - %s", url) + + # This was a PITA... + # Thanks to "Bulletproof SSL and TLS - Ivan Ristic" for helping me out + try: + output, _ = le_util.run_script( + ["openssl", "ocsp", + "-no_nonce", "-header", "Host", host, + "-issuer", chain_path, + "-cert", cert_path, + "-url", url, + "-CAfile", chain_path]) + except errors.SubprocessError: + return "(OCSP Failure)" + + return _translate_ocsp_query(cert_path, output) + + +def _translate_ocsp_query(cert_path, ocsp_output): + """Returns a label string out of the query.""" + if not "Response verify OK": + return "Revocation Unknown" + if cert_path + ": good" in ocsp_output: + return "" + elif cert_path + ": revoked" in ocsp_output: + return REV_LABEL + else: + raise errors.Error( + "Unable to properly parse OCSP output: %s", ocsp_output) + + From 40e29bb95f5ee64f55bea25be94a3b3341c99b53 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Dec 2016 14:38:20 -0800 Subject: [PATCH 309/331] begin implementing OCSP checking for "certificates" --- certbot/cert_manager.py | 8 ++++++++ certbot/ocsp.py | 14 ++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 35b12e1bb..1b6d441c7 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -8,6 +8,7 @@ import zope.component from certbot import errors from certbot import interfaces +from certbot import ocsp from certbot import storage from certbot import util @@ -170,11 +171,17 @@ def _report_human_readable(parsed_certs): certinfo = [] for cert in parsed_certs: now = pytz.UTC.fromutc(datetime.datetime.utcnow()) + expiration_text = "" if cert.is_test_cert: expiration_text = "INVALID: TEST CERT" elif cert.target_expiry <= now: expiration_text = "INVALID: EXPIRED" else: + revoked = ocsp.revoked_status(cert.cert, cert.chain) + if revoked: + expiration_text = "INVALID: " + revoked + + if not expiration_text: diff = cert.target_expiry - now if diff.days == 1: expiration_text = "VALID: 1 day" @@ -182,6 +189,7 @@ def _report_human_readable(parsed_certs): expiration_text = "VALID: {0} hour(s)".format(diff.seconds // 3600) else: expiration_text = "VALID: {0} days".format(diff.days) + valid_string = "{0} ({1})".format(cert.target_expiry, expiration_text) certinfo.append(" Certificate Name: {0}\n" " Domains: {1}\n" diff --git a/certbot/ocsp.py b/certbot/ocsp.py index d7fc06e0d..cb3dd0610 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -1,8 +1,8 @@ +"""Tools for checking certificate revocation.""" import logging -from letsencrypt import errors -from letsencrypt import le_util - +from certbot import errors +from certbot import util logger = logging.getLogger(__name__) @@ -10,6 +10,9 @@ logger = logging.getLogger(__name__) REV_LABEL = "**Revoked**" EXP_LABEL = "**Expired**" +INSTALL_LABEL = "(Installed)" + + def revoked_status(cert_path, chain_path): """Get revoked status for a particular cert version. @@ -19,7 +22,7 @@ def revoked_status(cert_path, chain_path): :param str chain_path: Path to chain certificate """ - url, _ = le_util.run_script( + url, _ = util.run_script( ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"]) url = url.rstrip() @@ -31,7 +34,7 @@ def revoked_status(cert_path, chain_path): # This was a PITA... # Thanks to "Bulletproof SSL and TLS - Ivan Ristic" for helping me out try: - output, _ = le_util.run_script( + output, _ = util.run_script( ["openssl", "ocsp", "-no_nonce", "-header", "Host", host, "-issuer", chain_path, @@ -56,4 +59,3 @@ def _translate_ocsp_query(cert_path, ocsp_output): raise errors.Error( "Unable to properly parse OCSP output: %s", ocsp_output) - From ac02cd9cb87ad8cd9e8080f13d7ccd36d9f0702b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 19 Dec 2016 17:36:37 -0800 Subject: [PATCH 310/331] ocsp checking needs -verify_other https://community.letsencrypt.org/t/unable-to-verify-ocsp-response/7264 --- certbot/ocsp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index cb3dd0610..f4c986609 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -40,9 +40,10 @@ def revoked_status(cert_path, chain_path): "-issuer", chain_path, "-cert", cert_path, "-url", url, - "-CAfile", chain_path]) + "-CAfile", chain_path, + "-verify_other", chain_path]) except errors.SubprocessError: - return "(OCSP Failure)" + return "OCSP Failure" return _translate_ocsp_query(cert_path, output) From 245b84ab783488002a11f82c735b1fd6d31614c3 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 19 Dec 2016 17:45:21 -0800 Subject: [PATCH 311/331] Format CLI to keep modern openssls happy - This is somewhat ominous --- certbot/ocsp.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index f4c986609..5a66ba48d 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -7,8 +7,7 @@ from certbot import util logger = logging.getLogger(__name__) -REV_LABEL = "**Revoked**" -EXP_LABEL = "**Expired**" +REV_LABEL = "REVOKED" INSTALL_LABEL = "(Installed)" @@ -36,7 +35,7 @@ def revoked_status(cert_path, chain_path): try: output, _ = util.run_script( ["openssl", "ocsp", - "-no_nonce", "-header", "Host", host, + "-no_nonce", "-header", "Host="+host, "-issuer", chain_path, "-cert", cert_path, "-url", url, From fe36e336a8bca509c9657904d7e9972682b028e8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 19 Dec 2016 18:37:01 -0800 Subject: [PATCH 312/331] Run with both old and new versions of openssl --- certbot/ocsp.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index 5a66ba48d..21b5d3be6 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -1,6 +1,8 @@ """Tools for checking certificate revocation.""" import logging +from subprocess import Popen, PIPE + from certbot import errors from certbot import util @@ -21,6 +23,7 @@ def revoked_status(cert_path, chain_path): :param str chain_path: Path to chain certificate """ + url, _ = util.run_script( ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"]) @@ -30,12 +33,21 @@ def revoked_status(cert_path, chain_path): raise errors.Error( "Unable to get OCSP host from cert, url - %s", url) - # This was a PITA... - # Thanks to "Bulletproof SSL and TLS - Ivan Ristic" for helping me out + # New versions of openssl want -header var=val, old ones want -header var val + test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"], + stdout=PIPE, stderr=PIPE) + _out, err = test_host_format.communicate() + if "Missing =" in err: + host_arg = ["Host=" + host] + else: + host_arg = ["Host", host] + + # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this! try: output, _ = util.run_script( ["openssl", "ocsp", - "-no_nonce", "-header", "Host="+host, + "-no_nonce", + "-header"] + host_arg + [ "-issuer", chain_path, "-cert", cert_path, "-url", url, From 7a18a124cec768f3405ea82667a1bdcec7566c10 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 19 Dec 2016 19:45:39 -0800 Subject: [PATCH 313/331] Better error handling --- certbot/cert_manager.py | 2 +- certbot/ocsp.py | 61 +++++++++++++++++++++++++++-------------- certbot/util.py | 7 +++-- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 1b6d441c7..4b2fc069f 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -172,12 +172,12 @@ def _report_human_readable(parsed_certs): for cert in parsed_certs: now = pytz.UTC.fromutc(datetime.datetime.utcnow()) expiration_text = "" + revoked = ocsp.revoked_status(cert.cert, cert.chain) if cert.is_test_cert: expiration_text = "INVALID: TEST CERT" elif cert.target_expiry <= now: expiration_text = "INVALID: EXPIRED" else: - revoked = ocsp.revoked_status(cert.cert, cert.chain) if revoked: expiration_text = "INVALID: " + revoked diff --git a/certbot/ocsp.py b/certbot/ocsp.py index 21b5d3be6..f1b2d7d32 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -24,50 +24,69 @@ def revoked_status(cert_path, chain_path): """ - url, _ = util.run_script( - ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"]) + if revoked_status.broken: + return False + + if not util.exe_exists("openssl"): + logging.info("openssl not installed, can't check revocation") + revoked_status.broken = True + return False + + try: + url, err = util.run_script( + ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"], + log=logging.debug) + except errors.SubprocessError: + logger.info("Cannot extract OCSP URI from %s", cert_path) + return False url = url.rstrip() host = url.partition("://")[2].rstrip("/") if not host: - raise errors.Error( - "Unable to get OCSP host from cert, url - %s", url) + logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path) + return False # New versions of openssl want -header var=val, old ones want -header var val test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"], stdout=PIPE, stderr=PIPE) _out, err = test_host_format.communicate() if "Missing =" in err: - host_arg = ["Host=" + host] + host_args = ["Host=" + host] else: - host_arg = ["Host", host] + host_args = ["Host", host] # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this! try: - output, _ = util.run_script( - ["openssl", "ocsp", - "-no_nonce", - "-header"] + host_arg + [ - "-issuer", chain_path, - "-cert", cert_path, - "-url", url, - "-CAfile", chain_path, - "-verify_other", chain_path]) + cmd = ["openssl", "ocsp", + "-no_nonce", + "-issuer", chain_path, + "-cert", cert_path, + "-url", url, + "-CAfile", chain_path, + "-verify_other", chain_path, + "-header"] + host_args + output, err = util.run_script(cmd, log=logging.debug) except errors.SubprocessError: - return "OCSP Failure" + logger.info("OCSP querying seems to be broken, assuming nothing is revoked...") + logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), err) + revoked_status.broken = True + return False - return _translate_ocsp_query(cert_path, output) + return _translate_ocsp_query(cert_path, output, err) +revoked_status.broken = False -def _translate_ocsp_query(cert_path, ocsp_output): +def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors): """Returns a label string out of the query.""" if not "Response verify OK": - return "Revocation Unknown" + logger.info("Revocation status for %s is unknown", cert_path) + logger.debug("Uncertain ouput:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors) + return "" if cert_path + ": good" in ocsp_output: return "" elif cert_path + ": revoked" in ocsp_output: return REV_LABEL else: - raise errors.Error( - "Unable to properly parse OCSP output: %s", ocsp_output) + logger.warn("Unable to properly parse OCSP output: %s", ocsp_output) + return "" diff --git a/certbot/util.py b/certbot/util.py index cbcfa3314..81a9beca1 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -38,10 +38,11 @@ ANSI_SGR_RED = "\033[31m" ANSI_SGR_RESET = "\033[0m" -def run_script(params): +def run_script(params, log=logger.error): """Run the script with the given params. :param list params: List of parameters to pass to Popen + :param logging.Logger log: Logger to use for errors """ try: @@ -51,7 +52,7 @@ def run_script(params): except (OSError, ValueError): msg = "Unable to run the command: %s" % " ".join(params) - logger.error(msg) + log(msg) raise errors.SubprocessError(msg) stdout, stderr = proc.communicate() @@ -60,7 +61,7 @@ def run_script(params): msg = "Error while running %s.\n%s\n%s" % ( " ".join(params), stdout, stderr) # Enter recovery routine... - logger.error(msg) + log(msg) raise errors.SubprocessError(msg) return stdout, stderr From 840c584cbdf8128c3e8715324bf27c59bd7f5bda Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 09:29:05 -0800 Subject: [PATCH 314/331] Make the OCSP checker a class (Since it contains a reasonable amount of system state) --- certbot/cert_manager.py | 7 ++- certbot/cli.py | 4 ++ certbot/ocsp.py | 108 ++++++++++++++++++++++++---------------- 3 files changed, 73 insertions(+), 46 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 4b2fc069f..59b46fe07 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -169,11 +169,14 @@ def _report_lines(msgs): def _report_human_readable(parsed_certs): """Format a results report for a parsed cert""" certinfo = [] + checker = ocsp.RevocationChecker() for cert in parsed_certs: now = pytz.UTC.fromutc(datetime.datetime.utcnow()) expiration_text = "" - revoked = ocsp.revoked_status(cert.cert, cert.chain) - if cert.is_test_cert: + revoked = checker.check_ocsp(cert.cert, cert.chain) + if revoked: + expiration_text = "INVALID: " + revoked + elif cert.is_test_cert: expiration_text = "INVALID: TEST CERT" elif cert.target_expiry <= now: expiration_text = "INVALID: EXPIRED" diff --git a/certbot/cli.py b/certbot/cli.py index 0adb7a4b5..fbcc8ff42 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -943,6 +943,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) + helpful.add( + ["testing", "certificates"], "--check-ocsp", default="if otherwise valid", + help='Whether to check OCSP for listed certs. Can be set to "never", "always",' + 'or "if otherwise valid".') helpful.add( ["testing", "standalone", "apache", "nginx"], "--tls-sni-01-port", type=int, default=flag_default("tls_sni_01_port"), diff --git a/certbot/ocsp.py b/certbot/ocsp.py index f1b2d7d32..f2fe1188f 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -13,50 +13,43 @@ REV_LABEL = "REVOKED" INSTALL_LABEL = "(Installed)" +class RevocationChecker(object): + "This class figures out OCSP checking on this system, and performs it." -def revoked_status(cert_path, chain_path): - """Get revoked status for a particular cert version. + def __init__(self): + self.broken = False - .. todo:: Make this a non-blocking call + if not util.exe_exists("openssl"): + logging.info("openssl not installed, can't check revocation") + self.broken = True - :param str cert_path: Path to certificate - :param str chain_path: Path to chain certificate + # New versions of openssl want -header var=val, old ones want -header var val + test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"], + stdout=PIPE, stderr=PIPE) + _out, err = test_host_format.communicate() + if "Missing =" in err: + self.host_args = lambda host: ["Host=" + host] + else: + self.host_args = lambda host: ["Host", host] - """ - if revoked_status.broken: - return False + def check_ocsp(self, cert_path, chain_path): + """Get revoked status for a particular cert version. - if not util.exe_exists("openssl"): - logging.info("openssl not installed, can't check revocation") - revoked_status.broken = True - return False + .. todo:: Make this a non-blocking call - try: - url, err = util.run_script( - ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"], - log=logging.debug) - except errors.SubprocessError: - logger.info("Cannot extract OCSP URI from %s", cert_path) - return False + :param str cert_path: Path to certificate + :param str chain_path: Path to intermediate cert - url = url.rstrip() - host = url.partition("://")[2].rstrip("/") - if not host: - logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path) - return False + """ + if self.broken: + return False - # New versions of openssl want -header var=val, old ones want -header var val - test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"], - stdout=PIPE, stderr=PIPE) - _out, err = test_host_format.communicate() - if "Missing =" in err: - host_args = ["Host=" + host] - else: - host_args = ["Host", host] - - # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this! - try: + logger.debug("Querying OCSP for %s", cert_path) + url, host = self.determine_ocsp_server(cert_path) + if not host: + return False + # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this! cmd = ["openssl", "ocsp", "-no_nonce", "-issuer", chain_path, @@ -64,16 +57,43 @@ def revoked_status(cert_path, chain_path): "-url", url, "-CAfile", chain_path, "-verify_other", chain_path, - "-header"] + host_args - output, err = util.run_script(cmd, log=logging.debug) - except errors.SubprocessError: - logger.info("OCSP querying seems to be broken, assuming nothing is revoked...") - logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), err) - revoked_status.broken = True - return False + "-header"] + self.host_args(host) + try: + output, err = util.run_script(cmd, log=logging.debug) + except errors.SubprocessError, e: + logger.info("We're offline, or OCSP querying is broken. " + "Assuming nothing is revoked...") + logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), e) + self.broken = True + return False - return _translate_ocsp_query(cert_path, output, err) -revoked_status.broken = False + return _translate_ocsp_query(cert_path, output, err) + + + def determine_ocsp_server(self, cert_path): + """Extract the OCSP server host from a certificate. + + :param str cert_path: Path to the cert we're checking OCSP for + :rtype tuple: + :returns: (OCSP server URL or None, OCSP server host or None) + + """ + try: + url, err = util.run_script( + ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"], + log=logging.debug) + except errors.SubprocessError: + logger.info("Cannot extract OCSP URI from %s", cert_path) + logger.debug("Error was:\n%s", err) + return None, None + + url = url.rstrip() + host = url.partition("://")[2].rstrip("/") + if host: + return url, host + else: + logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path) + return None, None def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors): From e5e5db24d72456632d6724ab6693a1e751385329 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 14:20:15 -0800 Subject: [PATCH 315/331] CLI flag for controlling ocsp checking now works --- certbot/cert_manager.py | 36 +++++++++++++----------------- certbot/cli.py | 4 ++-- certbot/ocsp.py | 27 +++++++++++++++++++++- certbot/tests/cert_manager_test.py | 12 +++++----- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 59b46fe07..ed8c6e76d 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -81,7 +81,7 @@ def certificates(config): parse_failures.append(renewal_file) # Describe all the certs - _describe_certs(parsed_certs, parse_failures) + _describe_certs(config, parsed_certs, parse_failures) def delete(config): """Delete Certbot files associated with a certificate lineage.""" @@ -166,34 +166,30 @@ def _report_lines(msgs): """Format a results report for a category of single-line renewal outcomes""" return " " + "\n ".join(str(msg) for msg in msgs) -def _report_human_readable(parsed_certs): +def _report_human_readable(config, parsed_certs): """Format a results report for a parsed cert""" certinfo = [] - checker = ocsp.RevocationChecker() + checker = ocsp.RevocationChecker(config) for cert in parsed_certs: now = pytz.UTC.fromutc(datetime.datetime.utcnow()) - expiration_text = "" - revoked = checker.check_ocsp(cert.cert, cert.chain) - if revoked: - expiration_text = "INVALID: " + revoked - elif cert.is_test_cert: - expiration_text = "INVALID: TEST CERT" - elif cert.target_expiry <= now: - expiration_text = "INVALID: EXPIRED" + status = "" + if cert.is_test_cert: + status = "INVALID: TEST_CERT" + if cert.target_expiry <= now: + status = status + ",EXPIRED" if status else "INVALID: EXPIRED" else: - if revoked: - expiration_text = "INVALID: " + revoked + status = checker.ocsp_status(cert.cert, cert.chain, status) - if not expiration_text: + if not status: diff = cert.target_expiry - now if diff.days == 1: - expiration_text = "VALID: 1 day" + status = "VALID: 1 day" elif diff.days < 1: - expiration_text = "VALID: {0} hour(s)".format(diff.seconds // 3600) + status = "VALID: {0} hour(s)".format(diff.seconds // 3600) else: - expiration_text = "VALID: {0} days".format(diff.days) + status = "VALID: {0} days".format(diff.days) - valid_string = "{0} ({1})".format(cert.target_expiry, expiration_text) + valid_string = "{0} ({1})".format(cert.target_expiry, status) certinfo.append(" Certificate Name: {0}\n" " Domains: {1}\n" " Expiry Date: {2}\n" @@ -206,7 +202,7 @@ def _report_human_readable(parsed_certs): cert.privkey)) return "\n".join(certinfo) -def _describe_certs(parsed_certs, parse_failures): +def _describe_certs(config, parsed_certs, parse_failures): """Print information about the certs we know about""" out = [] @@ -217,7 +213,7 @@ def _describe_certs(parsed_certs, parse_failures): else: if parsed_certs: notify("Found the following certs:") - notify(_report_human_readable(parsed_certs)) + notify(_report_human_readable(config, parsed_certs)) if parse_failures: notify("\nThe following renewal configuration files " "were invalid:") diff --git a/certbot/cli.py b/certbot/cli.py index fbcc8ff42..cea8af51b 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -944,9 +944,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) helpful.add( - ["testing", "certificates"], "--check-ocsp", default="if otherwise valid", + ["certificates", "testing"], "--check-ocsp", default="always", help='Whether to check OCSP for listed certs. Can be set to "never", "always",' - 'or "if otherwise valid".') + ' or "lazy" (ie, only for certs that are otherwise valid).') helpful.add( ["testing", "standalone", "apache", "nginx"], "--tls-sni-01-port", type=int, default=flag_default("tls_sni_01_port"), diff --git a/certbot/ocsp.py b/certbot/ocsp.py index f2fe1188f..c04afcc82 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -16,8 +16,9 @@ INSTALL_LABEL = "(Installed)" class RevocationChecker(object): "This class figures out OCSP checking on this system, and performs it." - def __init__(self): + def __init__(self, config): self.broken = False + self.config = config if not util.exe_exists("openssl"): logging.info("openssl not installed, can't check revocation") @@ -33,6 +34,30 @@ class RevocationChecker(object): self.host_args = lambda host: ["Host", host] + def ocsp_status(self, cert_path, chain_path, status_in): + """Helper function: updates a cert status string with revocation information + + :param str cert_path: path to a cert to check + :param str chain_path: issuing intermediate for the cert + :param str status_in: a string that is either empty, if the cert is otherwise + believed to be valid, or 'INVALID: $REASON'. + + :returns: a new status including revocation, if the cert is revoked.""" + + if self.config.check_ocsp.lower() == "never": + return status_in + elif self.config.check_ocsp.lower() == "lazy" and status_in: + return status_in + + revoked = self.check_ocsp(cert_path, chain_path) + if not revoked: + return status_in + elif status_in: + return status_in + ",REVOKED" + else: + return "INVALID: REVOKED" + + def check_ocsp(self, cert_path, chain_path): """Get revoked status for a particular cert version. diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index ae673fba1..7f4261fd0 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -189,31 +189,33 @@ class CertificatesTest(BaseCertManagerTest): cert.names.return_value = ["nameone", "nametwo"] cert.is_test_cert = False parsed_certs = [cert] + + mock_config = mock.MagicMock() # pylint: disable=protected-access - out = cert_manager._report_human_readable(parsed_certs) + out = cert_manager._report_human_readable(mock_config, parsed_certs) self.assertTrue("INVALID: EXPIRED" in out) cert.target_expiry += datetime.timedelta(hours=2) # pylint: disable=protected-access - out = cert_manager._report_human_readable(parsed_certs) + out = cert_manager._report_human_readable(mock_config, parsed_certs) self.assertTrue('1 hour(s)' in out) self.assertTrue('VALID' in out and not 'INVALID' in out) cert.target_expiry += datetime.timedelta(days=1) # pylint: disable=protected-access - out = cert_manager._report_human_readable(parsed_certs) + out = cert_manager._report_human_readable(mock_config, parsed_certs) self.assertTrue('1 day' in out) self.assertFalse('under' in out) self.assertTrue('VALID' in out and not 'INVALID' in out) cert.target_expiry += datetime.timedelta(days=2) # pylint: disable=protected-access - out = cert_manager._report_human_readable(parsed_certs) + out = cert_manager._report_human_readable(mock_config, parsed_certs) self.assertTrue('3 days' in out) self.assertTrue('VALID' in out and not 'INVALID' in out) cert.is_test_cert = True - out = cert_manager._report_human_readable(parsed_certs) + out = cert_manager._report_human_readable(mock_config, parsed_certs) self.assertTrue('INVALID: TEST CERT' in out) From 03f312e653e62b41ae639cc2878b5bf6536718c3 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 14:20:35 -0800 Subject: [PATCH 316/331] Allow filtering of "certbot certificates output" with --config-name or -d --- certbot/cert_manager.py | 7 ++++++- certbot/cli.py | 10 ++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index ed8c6e76d..86b5eedd9 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -171,6 +171,10 @@ def _report_human_readable(config, parsed_certs): certinfo = [] checker = ocsp.RevocationChecker(config) for cert in parsed_certs: + if config.certname and cert.lineagename != config.certname: + continue + if config.domains and not set(config.domains).issubset(cert.names()): + continue now = pytz.UTC.fromutc(datetime.datetime.utcnow()) status = "" if cert.is_test_cert: @@ -212,7 +216,8 @@ def _describe_certs(config, parsed_certs, parse_failures): notify("No certs found.") else: if parsed_certs: - notify("Found the following certs:") + match = "matching " if config.certname or config.domains else "" + notify("Found the following {0}certs:".format(match)) notify(_report_human_readable(config, parsed_certs)) if parse_failures: notify("\nThe following renewal configuration files " diff --git a/certbot/cli.py b/certbot/cli.py index cea8af51b..8e6d7910f 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -350,8 +350,10 @@ VERB_HELP = [ "usage": "\n\n certbot renew [--cert-name NAME] [options]\n\n" }), ("certificates", { - "short": "List all certificates managed by Certbot", - "opts": "List all certificates managed by Certbot" + "short": "List certificates managed by Certbot", + "opts": "List certificates managed by Certbot", + "usage": ("\n\n certbot certificates [options] ...\n\n" + "Print information about the status of certificates managed by Certbot.") }), ("delete", { "short": "Clean up all files related to a certificate", @@ -824,14 +826,14 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "being run in a terminal. This flag cannot be used with the " "renew subcommand.") helpful.add( - [None, "run", "certonly"], + [None, "run", "certonly", "certificates"], "-d", "--domains", "--domain", dest="domains", metavar="DOMAIN", action=_DomainsAction, default=[], help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains " "as a parameter. (default: Ask)") helpful.add( - [None, "run", "certonly", "manage", "rename", "delete"], + [None, "run", "certonly", "manage", "rename", "delete", "certificates"], "--cert-name", dest="certname", metavar="CERTNAME", default=None, help="Certificate name to apply. Only one certificate name can be used " From 15ed372df617caa8342493763b461b144eeb66b8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 14:32:39 -0800 Subject: [PATCH 317/331] Fix existing tests --- certbot/tests/cert_manager_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 7f4261fd0..4fd66c34a 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -179,7 +179,9 @@ class CertificatesTest(BaseCertManagerTest): self.assertTrue(mock_utility.called) shutil.rmtree(tempdir) - def test_report_human_readable(self): + @mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_status') + def test_report_human_readable(self, mock_ocsp): + mock_ocsp.side_effect = lambda _cert, _chain, status: status from certbot import cert_manager import datetime, pytz expiry = pytz.UTC.fromutc(datetime.datetime.utcnow()) @@ -190,7 +192,7 @@ class CertificatesTest(BaseCertManagerTest): cert.is_test_cert = False parsed_certs = [cert] - mock_config = mock.MagicMock() + mock_config = mock.MagicMock(certname=None, lineagename=None) # pylint: disable=protected-access out = cert_manager._report_human_readable(mock_config, parsed_certs) self.assertTrue("INVALID: EXPIRED" in out) @@ -216,7 +218,7 @@ class CertificatesTest(BaseCertManagerTest): cert.is_test_cert = True out = cert_manager._report_human_readable(mock_config, parsed_certs) - self.assertTrue('INVALID: TEST CERT' in out) + self.assertTrue('INVALID: TEST_CERT' in out) class SearchLineagesTest(BaseCertManagerTest): From bf6084db615afa28c5316c81ea146f869fd32d30 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 14:41:34 -0800 Subject: [PATCH 318/331] With mixed staging/prod lineages, it might not be correct to stop OCSPing - One lineage might fail, and a later one succeed --- certbot/ocsp.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index c04afcc82..56074fa45 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -86,10 +86,8 @@ class RevocationChecker(object): try: output, err = util.run_script(cmd, log=logging.debug) except errors.SubprocessError, e: - logger.info("We're offline, or OCSP querying is broken. " - "Assuming nothing is revoked...") + logger.info("We're offline, or OCSP querying is broken. ") logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), e) - self.broken = True return False return _translate_ocsp_query(cert_path, output, err) From 011f6055d4a4a011e166af195af5681be19c395a Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 14:43:35 -0800 Subject: [PATCH 319/331] Better message --- certbot/ocsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index 56074fa45..c26c8c1f3 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -86,7 +86,7 @@ class RevocationChecker(object): try: output, err = util.run_script(cmd, log=logging.debug) except errors.SubprocessError, e: - logger.info("We're offline, or OCSP querying is broken. ") + logger.info("OCSP check failed for %s (are we offline?)", cert_path) logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), e) return False From fcf7387c3d901ebe9223147805fab70c533c9805 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 14:54:48 -0800 Subject: [PATCH 320/331] Don't crash if openssl is missing --- certbot/ocsp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index c26c8c1f3..92dc6f872 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -23,6 +23,7 @@ class RevocationChecker(object): if not util.exe_exists("openssl"): logging.info("openssl not installed, can't check revocation") self.broken = True + return # New versions of openssl want -header var=val, old ones want -header var val test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"], From 7d02b8dbd541e2f35b325a5404b768d29aa36f48 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 15:00:21 -0800 Subject: [PATCH 321/331] py3fix --- certbot/ocsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index 92dc6f872..758fb2e2f 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -86,7 +86,7 @@ class RevocationChecker(object): "-header"] + self.host_args(host) try: output, err = util.run_script(cmd, log=logging.debug) - except errors.SubprocessError, e: + except errors.SubprocessError as e: logger.info("OCSP check failed for %s (are we offline?)", cert_path) logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), e) return False From 509f4029bb0b8c68a4c7eb26b1a6d3145734ba04 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 15:07:43 -0800 Subject: [PATCH 322/331] more py3 fixes --- certbot/ocsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index 758fb2e2f..9049ddc01 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -27,7 +27,7 @@ class RevocationChecker(object): # New versions of openssl want -header var=val, old ones want -header var val test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"], - stdout=PIPE, stderr=PIPE) + stdout=PIPE, stderr=PIPE, universal_newlines=True) _out, err = test_host_format.communicate() if "Missing =" in err: self.host_args = lambda host: ["Host=" + host] From f495863da975341474afc2389e5c1022e8cac330 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 15:38:37 -0800 Subject: [PATCH 323/331] Check --check-ocsp flags, and test those checks --- certbot/cli.py | 4 ++++ certbot/tests/cli_test.py | 4 ++++ certbot/util.py | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index 8e6d7910f..19d917739 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -529,6 +529,10 @@ class HelpfulArgumentParser(object): constants.FORCE_INTERACTIVE_FLAG)) parsed_args.noninteractive_mode = True + parsed_args.check_ocsp = parsed_args.check_ocsp.lower() + if parsed_args.check_ocsp not in ("always", "never", "lazy"): + raise errors.Error('--check-ocsp must be "always", "never", or "lazy"') + if parsed_args.force_interactive and parsed_args.noninteractive_mode: raise errors.Error( "Flag for non-interactive mode and {0} conflict".format( diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index e3bd28a5e..1714dcdbb 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -268,6 +268,10 @@ class ParseTest(unittest.TestCase): self.assertRaises( errors.Error, self.parse, "-n --force-interactive".split()) + def test_check_ocsp(self): + self.assertRaises(errors.Error, self.parse, "certificates --check-ocsp bogus".split()) + self.parse("certificates --check-ocsp lazy".split()) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" diff --git a/certbot/util.py b/certbot/util.py index 81a9beca1..cc0a74bd2 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -48,7 +48,8 @@ def run_script(params, log=logger.error): try: proc = subprocess.Popen(params, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE, + universal_newlines=True) except (OSError, ValueError): msg = "Unable to run the command: %s" % " ".join(params) From 76b8c53566cb979569b2368a6394e8bc95fd6c09 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 20 Dec 2016 16:22:14 -0800 Subject: [PATCH 324/331] Tests for ocsp.py, and associated fixes --- certbot/ocsp.py | 10 +-- certbot/tests/cli_test.py | 2 +- certbot/tests/ocsp_test.py | 137 +++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 certbot/tests/ocsp_test.py diff --git a/certbot/ocsp.py b/certbot/ocsp.py index 9049ddc01..f24d94266 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -45,9 +45,9 @@ class RevocationChecker(object): :returns: a new status including revocation, if the cert is revoked.""" - if self.config.check_ocsp.lower() == "never": + if self.config.check_ocsp == "never": return status_in - elif self.config.check_ocsp.lower() == "lazy" and status_in: + elif self.config.check_ocsp == "lazy" and status_in: return status_in revoked = self.check_ocsp(cert_path, chain_path) @@ -106,9 +106,9 @@ class RevocationChecker(object): url, err = util.run_script( ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"], log=logging.debug) - except errors.SubprocessError: + except errors.SubprocessError as e: logger.info("Cannot extract OCSP URI from %s", cert_path) - logger.debug("Error was:\n%s", err) + logger.debug("Error was:\n%s", e) return None, None url = url.rstrip() @@ -122,7 +122,7 @@ class RevocationChecker(object): def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors): """Returns a label string out of the query.""" - if not "Response verify OK": + if not "Response verify OK" in ocsp_errors: logger.info("Revocation status for %s is unknown", cert_path) logger.debug("Uncertain ouput:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors) return "" diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 1714dcdbb..b5c81bee4 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -270,7 +270,7 @@ class ParseTest(unittest.TestCase): def test_check_ocsp(self): self.assertRaises(errors.Error, self.parse, "certificates --check-ocsp bogus".split()) - self.parse("certificates --check-ocsp lazy".split()) + self.parse("certificates --check-ocsp LaZy".split()) class DefaultTest(unittest.TestCase): diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py new file mode 100644 index 000000000..0bc947021 --- /dev/null +++ b/certbot/tests/ocsp_test.py @@ -0,0 +1,137 @@ + +"""Tests for hooks.py""" +# pylint: disable=protected-access + +import os +import unittest + +import mock + +from certbot import errors +from certbot import hooks + +out = """Missing = in header key=value +ocsp: Use -help for summary. +""" + +class OCSPTest(unittest.TestCase): + + _multiprocess_can_split_ = True + + def setUp(self): + from certbot import ocsp + self.config = mock.MagicMock() + self.checker = ocsp.RevocationChecker(self.config) + + def tearDown(self): + pass + + @mock.patch('certbot.ocsp.logging.info') + @mock.patch('certbot.ocsp.Popen') + @mock.patch('certbot.util.exe_exists') + def test_init(self, mock_exists, mock_popen, mock_log): + mock_communicate = mock.MagicMock() + mock_communicate.communicate.return_value = (None, out) + mock_popen.return_value = mock_communicate + mock_exists.return_value = True + + from certbot import ocsp + checker = ocsp.RevocationChecker(self.config) + self.assertEqual(mock_popen.call_count, 1) + self.assertEqual(checker.host_args("x"), ["Host=x"]) + + mock_communicate.communicate.return_value = (None, out.partition("\n")[2]) + checker = ocsp.RevocationChecker(self.config) + self.assertEqual(checker.host_args("x"), ["Host", "x"]) + self.assertEqual(checker.broken, False) + + mock_exists.return_value = False + mock_popen.call_count = 0 + checker = ocsp.RevocationChecker(self.config) + self.assertEqual(mock_popen.call_count, 0) + self.assertEqual(mock_log.call_count, 1) + self.assertEqual(checker.broken, True) + + def test_ocsp_status(self): + from certbot import ocsp + checker = self.checker + checker.check_ocsp = mock.MagicMock() + checker.check_ocsp.return_value = "octopus found in certificate" + + checker.config.check_ocsp = "never" + self.assertEqual(checker.ocsp_status("a", "a", "xyz"), "xyz") + self.assertEqual(checker.ocsp_status("a", "a", ""), "") + self.assertEqual(checker.check_ocsp.call_count, 0) + + checker.config.check_ocsp = "lazy" + self.assertEqual(checker.ocsp_status("a", "a", "xyz"), "xyz") + self.assertEqual(checker.check_ocsp.call_count, 0) + self.assertEqual(checker.ocsp_status("a", "a", ""), "INVALID: REVOKED") + + checker.config.check_ocsp = "always" + self.assertEqual(checker.ocsp_status("a", "a", "xyz"), "xyz,REVOKED") + checker.check_ocsp.return_value = "" + self.assertEqual(checker.ocsp_status("a", "a", "xyz"), "xyz") + + + @mock.patch('certbot.ocsp.logger.debug') + @mock.patch('certbot.ocsp.logger.info') + @mock.patch('certbot.util.run_script') + def test_determine_ocsp_server(self, mock_run, mock_info, mock_debug): + uri = "http://ocsp.stg-int-x1.letsencrypt.org/" + host = "ocsp.stg-int-x1.letsencrypt.org" + mock_run.return_value = uri, "" + self.assertEquals(self.checker.determine_ocsp_server("beep"), (uri, host)) + mock_run.return_value = "ftp:/" + host + "/", "" + self.assertEquals(self.checker.determine_ocsp_server("beep"), (None, None)) + self.assertEquals(mock_info.call_count, 1) + + c = "confusion" + mock_run.side_effect = errors.SubprocessError(c) + self.assertEquals(self.checker.determine_ocsp_server("beep"), (None, None)) + self.assertTrue(c in mock_debug.call_args[0][1]) + + @mock.patch('certbot.ocsp.logger') + @mock.patch('certbot.util.run_script') + def test_translate_ocsp(self, mock_run, mock_log): + # pylint: disable=protected-access + mock_run.return_value = openssl_confused + from certbot import ocsp + self.assertEquals(ocsp._translate_ocsp_query(*openssl_happy), "") + self.assertEquals(ocsp._translate_ocsp_query(*openssl_confused), "") + self.assertEquals(mock_log.debug.call_count, 1) + self.assertEquals(mock_log.warn.call_count, 0) + self.assertEquals(ocsp._translate_ocsp_query(*openssl_broken), "") + self.assertEquals(mock_log.warn.call_count, 1) + self.assertEquals(ocsp._translate_ocsp_query(*openssl_revoked), "REVOKED") + + +openssl_confused = ("", """ +/etc/letsencrypt/live/example.org/cert.pem: good + This Update: Dec 17 00:00:00 2016 GMT + Next Update: Dec 24 00:00:00 2016 GMT +""", +""" +Response Verify Failure +139903674214048:error:27069065:OCSP routines:OCSP_basic_verify:certificate verify error:ocsp_vfy.c:138:Verify error:unable to get local issuer certificate +""") + +openssl_happy = ("blah.pem", """ +blah.pem: good + This Update: Dec 20 18:00:00 2016 GMT + Next Update: Dec 27 18:00:00 2016 GMT +""", +"Response verify OK") + +openssl_revoked = ("blah.pem", """ +blah.pem: revoked + This Update: Dec 20 01:00:00 2016 GMT + Next Update: Dec 27 01:00:00 2016 GMT + Revocation Time: Dec 20 01:46:34 2016 GMT +""", +"""Response verify OK""") + +openssl_broken = ("", "tentacles", "Response verify OK") + +if __name__ == '__main__': + unittest.main() # pragma: no cover From 0ed3213989c095455d4c17c91cd1795e794374ea Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Dec 2016 14:25:16 -0800 Subject: [PATCH 325/331] Remove --check-ocsp flag - Might have been occasionally useful, but simplicity - Add some missing tests, remove some obsolete ones --- certbot/cert_manager.py | 21 +++++---- certbot/cli.py | 8 ---- certbot/ocsp.py | 57 ++++++----------------- certbot/tests/cert_manager_test.py | 4 +- certbot/tests/cli_test.py | 4 -- certbot/tests/ocsp_test.py | 74 +++++++++++++++--------------- 6 files changed, 65 insertions(+), 103 deletions(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 86b5eedd9..09798e3bc 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -169,22 +169,25 @@ def _report_lines(msgs): def _report_human_readable(config, parsed_certs): """Format a results report for a parsed cert""" certinfo = [] - checker = ocsp.RevocationChecker(config) + checker = ocsp.RevocationChecker() for cert in parsed_certs: if config.certname and cert.lineagename != config.certname: continue if config.domains and not set(config.domains).issubset(cert.names()): continue now = pytz.UTC.fromutc(datetime.datetime.utcnow()) - status = "" - if cert.is_test_cert: - status = "INVALID: TEST_CERT" - if cert.target_expiry <= now: - status = status + ",EXPIRED" if status else "INVALID: EXPIRED" - else: - status = checker.ocsp_status(cert.cert, cert.chain, status) - if not status: + reasons = [] + if cert.is_test_cert: + reasons.append('TEST_CERT') + if cert.target_expiry <= now: + reasons.append('EXPIRED') + if checker.ocsp_revoked(cert.cert, cert.chain): + reasons.append('REVOKED') + + if reasons: + status = "INVALID: " + ", ".join(reasons) + else: diff = cert.target_expiry - now if diff.days == 1: status = "VALID: 1 day" diff --git a/certbot/cli.py b/certbot/cli.py index 19d917739..68eb67b35 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -529,10 +529,6 @@ class HelpfulArgumentParser(object): constants.FORCE_INTERACTIVE_FLAG)) parsed_args.noninteractive_mode = True - parsed_args.check_ocsp = parsed_args.check_ocsp.lower() - if parsed_args.check_ocsp not in ("always", "never", "lazy"): - raise errors.Error('--check-ocsp must be "always", "never", or "lazy"') - if parsed_args.force_interactive and parsed_args.noninteractive_mode: raise errors.Error( "Flag for non-interactive mode and {0} conflict".format( @@ -949,10 +945,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) - helpful.add( - ["certificates", "testing"], "--check-ocsp", default="always", - help='Whether to check OCSP for listed certs. Can be set to "never", "always",' - ' or "lazy" (ie, only for certs that are otherwise valid).') helpful.add( ["testing", "standalone", "apache", "nginx"], "--tls-sni-01-port", type=int, default=flag_default("tls_sni_01_port"), diff --git a/certbot/ocsp.py b/certbot/ocsp.py index f24d94266..a5df5743d 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -8,17 +8,11 @@ from certbot import util logger = logging.getLogger(__name__) - -REV_LABEL = "REVOKED" - -INSTALL_LABEL = "(Installed)" - class RevocationChecker(object): "This class figures out OCSP checking on this system, and performs it." - def __init__(self, config): + def __init__(self): self.broken = False - self.config = config if not util.exe_exists("openssl"): logging.info("openssl not installed, can't check revocation") @@ -35,46 +29,25 @@ class RevocationChecker(object): self.host_args = lambda host: ["Host", host] - def ocsp_status(self, cert_path, chain_path, status_in): - """Helper function: updates a cert status string with revocation information - - :param str cert_path: path to a cert to check - :param str chain_path: issuing intermediate for the cert - :param str status_in: a string that is either empty, if the cert is otherwise - believed to be valid, or 'INVALID: $REASON'. - - :returns: a new status including revocation, if the cert is revoked.""" - - if self.config.check_ocsp == "never": - return status_in - elif self.config.check_ocsp == "lazy" and status_in: - return status_in - - revoked = self.check_ocsp(cert_path, chain_path) - if not revoked: - return status_in - elif status_in: - return status_in + ",REVOKED" - else: - return "INVALID: REVOKED" - - - def check_ocsp(self, cert_path, chain_path): + def ocsp_revoked(self, cert_path, chain_path): """Get revoked status for a particular cert version. .. todo:: Make this a non-blocking call :param str cert_path: Path to certificate :param str chain_path: Path to intermediate cert + :rtype bool or None: + :returns: False if valid; True if revoked; None if check itself failed """ if self.broken: - return False + return None + logger.debug("Querying OCSP for %s", cert_path) url, host = self.determine_ocsp_server(cert_path) if not host: - return False + return None # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this! cmd = ["openssl", "ocsp", "-no_nonce", @@ -89,7 +62,7 @@ class RevocationChecker(object): except errors.SubprocessError as e: logger.info("OCSP check failed for %s (are we offline?)", cert_path) logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), e) - return False + return None return _translate_ocsp_query(cert_path, output, err) @@ -103,7 +76,7 @@ class RevocationChecker(object): """ try: - url, err = util.run_script( + url, _err = util.run_script( ["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"], log=logging.debug) except errors.SubprocessError as e: @@ -119,18 +92,18 @@ class RevocationChecker(object): logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path) return None, None - def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors): - """Returns a label string out of the query.""" + """Parse openssl's weird output to work out what it means.""" + if not "Response verify OK" in ocsp_errors: logger.info("Revocation status for %s is unknown", cert_path) logger.debug("Uncertain ouput:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors) - return "" + return None if cert_path + ": good" in ocsp_output: - return "" + return False elif cert_path + ": revoked" in ocsp_output: - return REV_LABEL + return True else: logger.warn("Unable to properly parse OCSP output: %s", ocsp_output) - return "" + return None diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 4fd66c34a..dc83d0952 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -179,9 +179,9 @@ class CertificatesTest(BaseCertManagerTest): self.assertTrue(mock_utility.called) shutil.rmtree(tempdir) - @mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_status') + @mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked') def test_report_human_readable(self, mock_ocsp): - mock_ocsp.side_effect = lambda _cert, _chain, status: status + mock_ocsp.side_effect = lambda _cert, _chain: None from certbot import cert_manager import datetime, pytz expiry = pytz.UTC.fromutc(datetime.datetime.utcnow()) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index b5c81bee4..e3bd28a5e 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -268,10 +268,6 @@ class ParseTest(unittest.TestCase): self.assertRaises( errors.Error, self.parse, "-n --force-interactive".split()) - def test_check_ocsp(self): - self.assertRaises(errors.Error, self.parse, "certificates --check-ocsp bogus".split()) - self.parse("certificates --check-ocsp LaZy".split()) - class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index 0bc947021..ce3ade2e8 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -2,13 +2,11 @@ """Tests for hooks.py""" # pylint: disable=protected-access -import os import unittest import mock from certbot import errors -from certbot import hooks out = """Missing = in header key=value ocsp: Use -help for summary. @@ -20,8 +18,13 @@ class OCSPTest(unittest.TestCase): def setUp(self): from certbot import ocsp - self.config = mock.MagicMock() - self.checker = ocsp.RevocationChecker(self.config) + with mock.patch('certbot.ocsp.Popen') as mock_popen: + with mock.patch('certbot.util.exe_exists') as mock_exists: + mock_communicate = mock.MagicMock() + mock_communicate.communicate.return_value = (None, out) + mock_popen.return_value = mock_communicate + mock_exists.return_value = True + self.checker = ocsp.RevocationChecker() def tearDown(self): pass @@ -36,44 +39,38 @@ class OCSPTest(unittest.TestCase): mock_exists.return_value = True from certbot import ocsp - checker = ocsp.RevocationChecker(self.config) + checker = ocsp.RevocationChecker() self.assertEqual(mock_popen.call_count, 1) self.assertEqual(checker.host_args("x"), ["Host=x"]) mock_communicate.communicate.return_value = (None, out.partition("\n")[2]) - checker = ocsp.RevocationChecker(self.config) + checker = ocsp.RevocationChecker() self.assertEqual(checker.host_args("x"), ["Host", "x"]) self.assertEqual(checker.broken, False) mock_exists.return_value = False mock_popen.call_count = 0 - checker = ocsp.RevocationChecker(self.config) + checker = ocsp.RevocationChecker() self.assertEqual(mock_popen.call_count, 0) self.assertEqual(mock_log.call_count, 1) self.assertEqual(checker.broken, True) - def test_ocsp_status(self): - from certbot import ocsp - checker = self.checker - checker.check_ocsp = mock.MagicMock() - checker.check_ocsp.return_value = "octopus found in certificate" + @mock.patch('certbot.ocsp.RevocationChecker.determine_ocsp_server') + @mock.patch('certbot.util.run_script') + def test_ocsp_revoked(self, mock_run, mock_determine): + self.checker.broken = True + mock_determine.return_value = ("", "") + self.assertEqual(self.checker.ocsp_revoked("x", "y"), None) - checker.config.check_ocsp = "never" - self.assertEqual(checker.ocsp_status("a", "a", "xyz"), "xyz") - self.assertEqual(checker.ocsp_status("a", "a", ""), "") - self.assertEqual(checker.check_ocsp.call_count, 0) + self.checker.broken = False + mock_run.return_value = tuple(openssl_happy[1:]) + self.assertEqual(self.checker.ocsp_revoked("x", "y"), None) + self.assertEqual(mock_run.call_count, 0) + + mock_determine.return_value = ("http://x.co", "x.co") + self.assertEqual(self.checker.ocsp_revoked("blah.pem", "chain.pem"), False) - checker.config.check_ocsp = "lazy" - self.assertEqual(checker.ocsp_status("a", "a", "xyz"), "xyz") - self.assertEqual(checker.check_ocsp.call_count, 0) - self.assertEqual(checker.ocsp_status("a", "a", ""), "INVALID: REVOKED") - checker.config.check_ocsp = "always" - self.assertEqual(checker.ocsp_status("a", "a", "xyz"), "xyz,REVOKED") - checker.check_ocsp.return_value = "" - self.assertEqual(checker.ocsp_status("a", "a", "xyz"), "xyz") - - @mock.patch('certbot.ocsp.logger.debug') @mock.patch('certbot.ocsp.logger.info') @mock.patch('certbot.util.run_script') @@ -81,31 +78,32 @@ class OCSPTest(unittest.TestCase): uri = "http://ocsp.stg-int-x1.letsencrypt.org/" host = "ocsp.stg-int-x1.letsencrypt.org" mock_run.return_value = uri, "" - self.assertEquals(self.checker.determine_ocsp_server("beep"), (uri, host)) + self.assertEqual(self.checker.determine_ocsp_server("beep"), (uri, host)) mock_run.return_value = "ftp:/" + host + "/", "" - self.assertEquals(self.checker.determine_ocsp_server("beep"), (None, None)) - self.assertEquals(mock_info.call_count, 1) + self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None)) + self.assertEqual(mock_info.call_count, 1) c = "confusion" mock_run.side_effect = errors.SubprocessError(c) - self.assertEquals(self.checker.determine_ocsp_server("beep"), (None, None)) + self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None)) self.assertTrue(c in mock_debug.call_args[0][1]) @mock.patch('certbot.ocsp.logger') @mock.patch('certbot.util.run_script') def test_translate_ocsp(self, mock_run, mock_log): - # pylint: disable=protected-access + # pylint: disable=protected-access,star-args mock_run.return_value = openssl_confused from certbot import ocsp - self.assertEquals(ocsp._translate_ocsp_query(*openssl_happy), "") - self.assertEquals(ocsp._translate_ocsp_query(*openssl_confused), "") - self.assertEquals(mock_log.debug.call_count, 1) - self.assertEquals(mock_log.warn.call_count, 0) - self.assertEquals(ocsp._translate_ocsp_query(*openssl_broken), "") - self.assertEquals(mock_log.warn.call_count, 1) - self.assertEquals(ocsp._translate_ocsp_query(*openssl_revoked), "REVOKED") + self.assertEqual(ocsp._translate_ocsp_query(*openssl_happy), False) + self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), None) + self.assertEqual(mock_log.debug.call_count, 1) + self.assertEqual(mock_log.warn.call_count, 0) + self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), None) + self.assertEqual(mock_log.warn.call_count, 1) + self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True) +# pylint: disable=line-too-long openssl_confused = ("", """ /etc/letsencrypt/live/example.org/cert.pem: good This Update: Dec 17 00:00:00 2016 GMT From e2d8630f5e3658218ce8fa3256dae841a1257ffe Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Dec 2016 14:42:35 -0800 Subject: [PATCH 326/331] py3fix --- certbot/tests/ocsp_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index ce3ade2e8..285b0379b 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -86,7 +86,7 @@ class OCSPTest(unittest.TestCase): c = "confusion" mock_run.side_effect = errors.SubprocessError(c) self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None)) - self.assertTrue(c in mock_debug.call_args[0][1]) + self.assertTrue(c in repr(mock_debug.call_args[0][1])) @mock.patch('certbot.ocsp.logger') @mock.patch('certbot.util.run_script') From 61e822a89724a3430777a9840404e4348a5aa1ff Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Dec 2016 21:50:19 -0800 Subject: [PATCH 327/331] Add a few more tests --- certbot/tests/cert_manager_test.py | 35 +++++++++++++++++++++++++----- certbot/tests/ocsp_test.py | 3 +++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index dc83d0952..003eb1470 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -1,6 +1,7 @@ """Tests for certbot.cert_manager.""" # pylint disable=protected-access import os +import re import shutil import tempfile import unittest @@ -181,7 +182,7 @@ class CertificatesTest(BaseCertManagerTest): @mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked') def test_report_human_readable(self, mock_ocsp): - mock_ocsp.side_effect = lambda _cert, _chain: None + mock_ocsp.return_value = None from certbot import cert_manager import datetime, pytz expiry = pytz.UTC.fromutc(datetime.datetime.utcnow()) @@ -191,35 +192,57 @@ class CertificatesTest(BaseCertManagerTest): cert.names.return_value = ["nameone", "nametwo"] cert.is_test_cert = False parsed_certs = [cert] + get_report = lambda: cert_manager._report_human_readable(mock_config, parsed_certs) mock_config = mock.MagicMock(certname=None, lineagename=None) # pylint: disable=protected-access - out = cert_manager._report_human_readable(mock_config, parsed_certs) + out = get_report() self.assertTrue("INVALID: EXPIRED" in out) cert.target_expiry += datetime.timedelta(hours=2) # pylint: disable=protected-access - out = cert_manager._report_human_readable(mock_config, parsed_certs) + out = get_report() self.assertTrue('1 hour(s)' in out) self.assertTrue('VALID' in out and not 'INVALID' in out) cert.target_expiry += datetime.timedelta(days=1) # pylint: disable=protected-access - out = cert_manager._report_human_readable(mock_config, parsed_certs) + out = get_report() self.assertTrue('1 day' in out) self.assertFalse('under' in out) self.assertTrue('VALID' in out and not 'INVALID' in out) cert.target_expiry += datetime.timedelta(days=2) # pylint: disable=protected-access - out = cert_manager._report_human_readable(mock_config, parsed_certs) + out = get_report() self.assertTrue('3 days' in out) self.assertTrue('VALID' in out and not 'INVALID' in out) cert.is_test_cert = True - out = cert_manager._report_human_readable(mock_config, parsed_certs) + out = get_report() self.assertTrue('INVALID: TEST_CERT' in out) + cert = mock.MagicMock(lineagename="indescribable") + cert.target_expiry = expiry + cert.names.return_value = ["nameone", "thrice.named"] + cert.is_test_cert = True + parsed_certs.append(cert) + + out = get_report() + self.assertEqual(len(re.findall("INVALID:", out)), 2) + mock_config.domains = ["thrice.named"] + out = get_report() + self.assertEqual(len(re.findall("INVALID:", out)), 1) + mock_config.domains = ["nameone"] + out = get_report() + self.assertEqual(len(re.findall("INVALID:", out)), 2) + mock_config.certname = "indescribable" + out = get_report() + self.assertEqual(len(re.findall("INVALID:", out)), 1) + mock_config.certname = "horror" + out = get_report() + self.assertEqual(len(re.findall("INVALID:", out)), 0) + class SearchLineagesTest(BaseCertManagerTest): """Tests for certbot.cert_manager._search_lineages.""" diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index 285b0379b..e136978d8 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -69,6 +69,9 @@ class OCSPTest(unittest.TestCase): mock_determine.return_value = ("http://x.co", "x.co") self.assertEqual(self.checker.ocsp_revoked("blah.pem", "chain.pem"), False) + mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher") + self.assertEqual(self.checker.ocsp_revoked("x", "y"), None) + self.assertEqual(mock_run.call_count, 2) @mock.patch('certbot.ocsp.logger.debug') From 7014ab5fd0f02530b2319373f5fd45824a4d7a29 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 21 Dec 2016 23:20:19 -0800 Subject: [PATCH 328/331] lint --- certbot/tests/cert_manager_test.py | 4 +++- certbot/tests/ocsp_test.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 003eb1470..6caaab878 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -1,5 +1,5 @@ """Tests for certbot.cert_manager.""" -# pylint disable=protected-access +# pylint: disable=protected-access import os import re import shutil @@ -192,6 +192,8 @@ class CertificatesTest(BaseCertManagerTest): cert.names.return_value = ["nameone", "nametwo"] cert.is_test_cert = False parsed_certs = [cert] + + # pylint: disable=protected-access get_report = lambda: cert_manager._report_human_readable(mock_config, parsed_certs) mock_config = mock.MagicMock(certname=None, lineagename=None) diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index e136978d8..c4517174d 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -1,5 +1,4 @@ - -"""Tests for hooks.py""" +"""Tests for ocsp.py""" # pylint: disable=protected-access import unittest From 39f55513054609fbad0d87fbf2d493665b180706 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 22 Dec 2016 08:24:08 -0800 Subject: [PATCH 329/331] Merge the manual and script plugins (#3890) * Start of combined manual/script plugin * Return str from hooks.execute, not bytes * finish manual/script rewrite * delete old manual and script plugins * manually specify we want chall.token * use consistent quotes * specify chall for uri * s/script/hook * fix spacing on instructions * remove unneeded response argument * make achall more helpful * simplify perform * remove old test files * add start of manual_tests * fix ParseTest.test_help * stop using manual_test_mode in cli tests * Revert "make achall more helpful" This reverts commit 54b01cea30167065e3682834a71144b81e96c07f. * use bad response/validation methods on achalls * simplify perform and cleanup environment * finish manual tests * Add HTTP manual hook integration test * add manual http scripts * Add manual DNS script integration test * remove references to the script plugin * they're hooks, not scripts * add --manual-public-ip-logging-ok to integration tests * use --pref-chall for dns integration * does dns work? * validate hooks * test hook validation * Revert "does dns work?" This reverts commit 1224cc2961b35a2b8e9e5d2ca3af7c081161b22a. * busy wait in manual-http-auth * remove DNS script test for now * Fix challenge prefix and add trailing . * Add comment about universal_newlines * Fix typo from 0464ba2c4 * fix nits and typos * Generalize HookCOmmandNotFound error * Add verify_exe_exists * Don't duplicate code in hooks.py * Revert changes to hooks.py * Use consistent hook error messages --- certbot/cli.py | 6 +- certbot/hooks.py | 5 +- certbot/plugins/manual.py | 305 +++++++++++---------------------- certbot/plugins/manual_test.py | 180 +++++++++---------- certbot/plugins/script.py | 161 ----------------- certbot/plugins/script_test.py | 170 ------------------ certbot/plugins/selection.py | 4 +- certbot/tests/acme_util.py | 11 ++ certbot/tests/cli_test.py | 22 +-- setup.py | 1 - tests/boulder-integration.sh | 7 +- tests/integration/_common.sh | 2 +- tests/manual-dns-auth.sh | 4 + tests/manual-http-auth.sh | 12 ++ tests/manual-http-cleanup.sh | 2 + 15 files changed, 237 insertions(+), 655 deletions(-) delete mode 100644 certbot/plugins/script.py delete mode 100644 certbot/plugins/script_test.py create mode 100755 tests/manual-dns-auth.sh create mode 100755 tests/manual-http-auth.sh create mode 100755 tests/manual-http-cleanup.sh diff --git a/certbot/cli.py b/certbot/cli.py index 0adb7a4b5..8b26568c6 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -72,7 +72,7 @@ obtain, install, and renew certificates: --standalone Run a standalone webserver for authentication %s --webroot Place files in a server's webroot folder for authentication - --manual Obtain certs interactively, or using shell script hoooks + --manual Obtain certs interactively, or using shell script hooks -n Run non-interactively --test-cert Obtain a test cert from a staging server @@ -100,7 +100,7 @@ More detailed help: all, automation, commands, paths, security, testing, or any of the subcommands or plugins (certonly, renew, install, register, nginx, - apache, standalone, webroot, script, etc.) + apache, standalone, webroot, etc.) """ @@ -1153,8 +1153,6 @@ def _plugins_parsing(helpful, plugins): "--nginx", action="store_true", help="Obtain and install certs using Nginx") helpful.add(["plugins", "certonly"], "--standalone", action="store_true", help='Obtain certs using a "standalone" webserver.') - helpful.add(["plugins", "certonly"], "--script", action="store_true", - help='Obtain certs using shell script(s)') helpful.add(["plugins", "certonly"], "--manual", action="store_true", help='Provide laborious manual instructions for obtaining a cert') helpful.add(["plugins", "certonly"], "--webroot", action="store_true", diff --git a/certbot/hooks.py b/certbot/hooks.py index 37afee9b0..63afba091 100644 --- a/certbot/hooks.py +++ b/certbot/hooks.py @@ -84,7 +84,10 @@ def execute(shell_cmd): :returns: `tuple` (`str` stderr, `str` stdout)""" - cmd = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE) + # universal_newlines causes Popen.communicate() + # to return str objects instead of bytes in Python 3 + cmd = Popen(shell_cmd, shell=True, stdout=PIPE, + stderr=PIPE, universal_newlines=True) out, err = cmd.communicate() if cmd.returncode != 0: logger.error('Hook command "%s" returned error code %d', diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 646b1d340..1163e7e7e 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -1,56 +1,49 @@ -"""Manual plugin.""" +"""Manual authenticator plugin""" import os -import logging -import pipes -import shutil -import socket -import subprocess -import sys -import tempfile -import time -import six import zope.component import zope.interface from acme import challenges -from acme import errors as acme_errors -from certbot import errors from certbot import interfaces +from certbot import errors +from certbot import hooks from certbot.plugins import common -logger = logging.getLogger(__name__) - - @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): - """Manual Authenticator. + """Manual authenticator - This plugin requires user's manual intervention in setting up a HTTP - server for solving http-01 challenges and thus does not need to be - run as a privileged process. Alternatively shows instructions on how - to use Python's built-in HTTP server. - - .. todo:: Support for `~.challenges.TLSSNI01`. + This plugin allows the user to perform the domain validation + challenge(s) themselves. This either be done manually by the user or + through shell scripts provided to Certbot. """ + + description = 'Manual configuration or run your own shell scripts' hidden = True - - description = "Manually configure an HTTP server" - - MESSAGE_TEMPLATE = { - "dns-01": """\ + long_description = ( + 'Authenticate through manual configuration or custom shell scripts. ' + 'When using shell scripts, an authenticator script must be provided. ' + 'The environment variables available to this script are ' + '$CERTBOT_DOMAIN which contains the domain being authenticated, ' + '$CERTBOT_VALIDATION which is the validation string, and ' + '$CERTBOT_TOKEN which is the filename of the resource requested when ' + 'performing an HTTP-01 challenge. An additional cleanup script can ' + 'also be provided and can use the additional variable ' + '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth ' + 'script.') + _DNS_INSTRUCTIONS = """\ Please deploy a DNS TXT record under the name {domain} with the following value: {validation} -Once this is deployed, -""", - "http-01": """\ +Once this is deployed,""" + _HTTP_INSTRUCTIONS = """\ Make sure your web server displays the following content at {uri} before continuing: @@ -59,204 +52,114 @@ Make sure your web server displays the following content at If you don't have HTTP server configured, you can run the following command on the target server (as root): -{command} -"""} - - # a disclaimer about your current IP being transmitted to Let's Encrypt's servers. - IP_DISCLAIMER = """\ -NOTE: The IP of this machine will be publicly logged as having requested this certificate. \ -If you're running certbot in manual mode on a machine that is not your server, \ -please ensure you're okay with that. - -Are you OK with your IP being logged? -""" - - # "cd /tmp/certbot" makes sure user doesn't serve /root, - # separate "public_html" ensures that cert.pem/key.pem are not - # served and makes it more obvious that Python command will serve - # anything recursively under the cwd - - CMD_TEMPLATE = """\ -mkdir -p {root}/public_html/{achall.URI_ROOT_PATH} -cd {root}/public_html +mkdir -p /tmp/certbot/public_html/{achall.URI_ROOT_PATH} +cd /tmp/certbot/public_html printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token} # run only once per server: $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ "import BaseHTTPServer, SimpleHTTPServer; \\ s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ s.serve_forever()" """ - """Command template.""" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - self._root = (tempfile.mkdtemp() if self.conf("test-mode") - else "/tmp/certbot") - self._httpd = None + self.env = dict() @classmethod def add_parser_arguments(cls, add): - add("test-mode", action="store_true", - help="Test mode. Executes the manual command in subprocess.") - add("public-ip-logging-ok", action="store_true", - help="Automatically allows public IP logging. (default: Ask)") + add('auth-hook', + help='Path or command to execute for the authentication script') + add('cleanup-hook', + help='Path or command to execute for the cleanup script') + add('public-ip-logging-ok', action='store_true', + help='Automatically allows public IP logging (default: Ask)') - def prepare(self): # pylint: disable=missing-docstring,no-self-use - if self.config.noninteractive_mode and not self.conf("test-mode"): - raise errors.PluginError("Running manual mode non-interactively is not supported") + def prepare(self): # pylint: disable=missing-docstring + if self.config.noninteractive_mode and not self.conf('auth-hook'): + raise errors.PluginError( + 'An authentication script must be provided with --{0} when ' + 'using the manual plugin non-interactively.'.format( + self.option_name('auth-hook'))) + self._validate_hooks() + + def _validate_hooks(self): + if self.config.validate_hooks: + for name in ('auth-hook', 'cleanup-hook'): + hook = self.conf(name) + if hook is not None: + hook_prefix = self.option_name(name)[:-len('-hook')] + hooks.validate_hook(hook, hook_prefix) def more_info(self): # pylint: disable=missing-docstring,no-self-use - return ("This plugin requires user's manual intervention in setting " - "up challenges to prove control of a domain and does not need " - "to be run as a privileged process. When solving " - "http-01 challenges, the user is responsible for setting up " - "an HTTP server. Alternatively, instructions are shown on how " - "to use Python's built-in HTTP server. The user is " - "responsible for configuration of a domain's DNS when solving " - "dns-01 challenges. The type of challenges used can be " - "controlled through the --preferred-challenges flag.") + return ( + 'This plugin allows the user to customize setup for domain ' + 'validation challenges either through shell scripts provided by ' + 'the user or by performing the setup manually.') def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument return [challenges.HTTP01, challenges.DNS01] - def perform(self, achalls): - # pylint: disable=missing-docstring - self._get_ip_logging_permission() - mapping = {"http-01": self._perform_http01_challenge, - "dns-01": self._perform_dns01_challenge} + def perform(self, achalls): # pylint: disable=missing-docstring + self._verify_ip_logging_ok() + + if self.conf('auth-hook'): + perform_achall = self._perform_achall_with_script + else: + perform_achall = self._perform_achall_manually + responses = [] - # TODO: group achalls by the same socket.gethostbyname(_ex) - # and prompt only once per server (one "echo -n" per domain) for achall in achalls: - responses.append(mapping[achall.typ](achall)) + perform_achall(achall) + responses.append(achall.response(achall.account_key)) return responses - @classmethod - def _test_mode_busy_wait(cls, port): - while True: - time.sleep(1) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect(("localhost", port)) - except socket.error: # pragma: no cover - pass + def _verify_ip_logging_ok(self): + if not self.conf('public-ip-logging-ok'): + cli_flag = '--{0}'.format(self.option_name('public-ip-logging-ok')) + msg = ('NOTE: The IP of this machine will be publicly logged as ' + "having requested this certificate. If you're running " + 'certbot in manual mode on a machine that is not your ' + "server, please ensure you're okay with that.\n\n" + 'Are you OK with your IP being logged?') + display = zope.component.getUtility(interfaces.IDisplay) + if display.yesno(msg, cli_flag=cli_flag, force_interactive=True): + setattr(self.config, self.dest('public-ip-logging-ok'), True) else: - break - finally: - sock.close() + raise errors.PluginError('Must agree to IP logging to proceed') - def cleanup(self, achalls): - # pylint: disable=missing-docstring - for achall in achalls: - if isinstance(achall.chall, challenges.HTTP01): - self._cleanup_http01_challenge(achall) - - def _perform_http01_challenge(self, achall): - # same path for each challenge response would be easier for - # users, but will not work if multiple domains point at the - # same server: default command doesn't support virtual hosts - response, validation = achall.response_and_validation() - - port = (response.port if self.config.http01_port is None - else int(self.config.http01_port)) - command = self.CMD_TEMPLATE.format( - root=self._root, achall=achall, response=response, - # TODO(kuba): pipes still necessary? - validation=pipes.quote(validation), - encoded_token=achall.chall.encode("token"), - port=port) - if self.conf("test-mode"): - logger.debug("Test mode. Executing the manual command: %s", command) - # sh shipped with OS X does't support echo -n, but supports printf - try: - self._httpd = subprocess.Popen( - command, - # don't care about setting stdout and stderr, - # we're in test mode anyway - shell=True, - executable=None, - # "preexec_fn" is UNIX specific, but so is "command" - preexec_fn=os.setsid) - except OSError as error: # ValueError should not happen! - logger.debug( - "Couldn't execute manual command: %s", error, exc_info=True) - return False - logger.debug("Manual command running as PID %s.", self._httpd.pid) - # give it some time to bootstrap, before we try to verify - # (cert generation in case of simpleHttpS might take time) - self._test_mode_busy_wait(port) - - if self._httpd.poll() is not None: - raise errors.Error("Couldn't execute manual command") + def _perform_achall_with_script(self, achall): + env = dict(CERTBOT_DOMAIN=achall.domain, + CERTBOT_VALIDATION=achall.validation(achall.account_key)) + if isinstance(achall.chall, challenges.HTTP01): + env['CERTBOT_TOKEN'] = achall.chall.encode('token') else: - self._notify_and_wait( - self._get_message(achall).format( - validation=validation, - response=response, - uri=achall.chall.uri(achall.domain), - command=command)) + os.environ.pop('CERTBOT_TOKEN', None) + os.environ.update(env) + _, out = hooks.execute(self.conf('auth-hook')) + env['CERTBOT_AUTH_OUTPUT'] = out.strip() + self.env[achall.domain] = env - if not response.simple_verify( - achall.chall, achall.domain, - achall.account_key.public_key(), self.config.http01_port): - logger.warning("Self-verify of challenge failed.") - - return response - - def _perform_dns01_challenge(self, achall): - response, validation = achall.response_and_validation() - if not self.conf("test-mode"): - self._notify_and_wait( - self._get_message(achall).format( - validation=validation, - domain=achall.validation_domain_name(achall.domain), - response=response)) - - try: - verification_status = response.simple_verify( - achall.chall, achall.domain, - achall.account_key.public_key()) - except acme_errors.DependencyError: - logger.warning("Self verification requires optional " - "dependency `dnspython` to be installed.") + def _perform_achall_manually(self, achall): + validation = achall.validation(achall.account_key) + if isinstance(achall.chall, challenges.HTTP01): + msg = self._HTTP_INSTRUCTIONS.format( + achall=achall, encoded_token=achall.chall.encode('token'), + port=self.config.http01_port, + uri=achall.chall.uri(achall.domain), validation=validation) else: - if not verification_status: - logger.warning("Self-verify of challenge failed.") + assert isinstance(achall.chall, challenges.DNS01) + msg = self._DNS_INSTRUCTIONS.format( + domain=achall.validation_domain_name(achall.domain), + validation=validation) + display = zope.component.getUtility(interfaces.IDisplay) + display.notification(msg, wrap=False, force_interactive=True) - return response - - def _cleanup_http01_challenge(self, achall): - # pylint: disable=missing-docstring,unused-argument - if self.conf("test-mode"): - assert self._httpd is not None, ( - "cleanup() must be called after perform()") - if self._httpd.poll() is None: - logger.debug("Terminating manual command process") - self._httpd.terminate() - else: - logger.debug("Manual command process already terminated " - "with %s code", self._httpd.returncode) - shutil.rmtree(self._root) - - def _notify_and_wait(self, message): - # pylint: disable=no-self-use - # TODO: IDisplay wraps messages, breaking the command - #answer = zope.component.getUtility(interfaces.IDisplay).notification( - # message=message, pause=True) - sys.stdout.write(message) - six.moves.input("Press ENTER to continue") - - def _get_ip_logging_permission(self): - # pylint: disable=missing-docstring - if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")): - if not zope.component.getUtility(interfaces.IDisplay).yesno( - self.IP_DISCLAIMER, "Yes", "No", - cli_flag="--manual-public-ip-logging-ok", - force_interactive=True): - raise errors.PluginError("Must agree to IP logging to proceed") - else: - self.config.namespace.manual_public_ip_logging_ok = True - - def _get_message(self, achall): - # pylint: disable=missing-docstring,no-self-use,unused-argument - return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "") + def cleanup(self, achalls): # pylint: disable=missing-docstring + if self.conf('cleanup-hook'): + for achall in achalls: + env = self.env.pop(achall.domain) + if 'CERTBOT_TOKEN' not in env: + os.environ.pop('CERTBOT_TOKEN', None) + os.environ.update(env) + hooks.execute(self.conf('cleanup-hook')) diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index 154b0d729..247352256 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -1,134 +1,112 @@ -"""Tests for certbot.plugins.manual.""" +"""Tests for certbot.plugins.manual""" +import os import unittest +import six import mock from acme import challenges -from acme import errors as acme_errors -from acme import jose -from certbot import achallenges from certbot import errors - from certbot.tests import acme_util -from certbot.tests import util as test_util - - -KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.manual.Authenticator.""" def setUp(self): - from certbot.plugins.manual import Authenticator + self.http_achall = acme_util.HTTP01_A + self.dns_achall = acme_util.DNS01_A + self.achalls = [self.http_achall, self.dns_achall] self.config = mock.MagicMock( - http01_port=8080, manual_test_mode=False, - manual_public_ip_logging_ok=False, noninteractive_mode=True) - self.auth = Authenticator(config=self.config, name="manual") + http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None, + manual_public_ip_logging_ok=False, noninteractive_mode=False, + validate_hooks=False) - self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY) - self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY) + from certbot.plugins.manual import Authenticator + self.auth = Authenticator(self.config, name='manual') - self.achalls = [self.http01, self.dns01] - - config_test_mode = mock.MagicMock( - http01_port=8080, manual_test_mode=True, noninteractive_mode=True) - self.auth_test_mode = Authenticator( - config=config_test_mode, name="manual") - - def test_prepare(self): + def test_prepare_no_hook_noninteractive(self): + self.config.noninteractive_mode = True self.assertRaises(errors.PluginError, self.auth.prepare) - self.auth_test_mode.prepare() # error not raised + + def test_prepare_bad_hook(self): + self.config.manual_auth_hook = os.path.abspath(os.sep) # is / on UNIX + self.config.validate_hooks = True + self.assertRaises(errors.HookCommandNotFound, self.auth.prepare) def test_more_info(self): - self.assertTrue(isinstance(self.auth.more_info(), str)) + self.assertTrue(isinstance(self.auth.more_info(), six.string_types)) def test_get_chall_pref(self): - self.assertTrue(all(issubclass(pref, challenges.Challenge) - for pref in self.auth.get_chall_pref("foo.com"))) + self.assertEqual(self.auth.get_chall_pref('example.org'), + [challenges.HTTP01, challenges.DNS01]) - @mock.patch("certbot.plugins.manual.zope.component.getUtility") - def test_perform_empty(self, mock_interaction): - mock_interaction().yesno.return_value = True - self.assertEqual([], self.auth.perform([])) + @mock.patch('certbot.plugins.manual.zope.component.getUtility') + def test_ip_logging_not_ok(self, mock_get_utility): + mock_get_utility().yesno.return_value = False + self.assertRaises(errors.PluginError, self.auth.perform, []) - @mock.patch("certbot.plugins.manual.zope.component.getUtility") - @mock.patch("certbot.plugins.manual.sys.stdout") - @mock.patch("acme.challenges.HTTP01Response.simple_verify") - @mock.patch("six.moves.input") - def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction): - mock_verify.return_value = True - mock_interaction().yesno.return_value = True + @mock.patch('certbot.plugins.manual.zope.component.getUtility') + def test_ip_logging_ok(self, mock_get_utility): + mock_get_utility().yesno.return_value = True + self.auth.perform([]) + self.assertTrue(self.config.manual_public_ip_logging_ok) - resp_http = self.http01.response(KEY) - resp_dns = self.dns01.response(KEY) + def test_script_perform(self): + self.config.manual_public_ip_logging_ok = True + self.config.manual_auth_hook = ( + 'echo $CERTBOT_DOMAIN; echo ${CERTBOT_TOKEN:-notoken}; ' + 'echo $CERTBOT_VALIDATION;') + dns_expected = '{0}\n{1}\n{2}'.format( + self.dns_achall.domain, 'notoken', + self.dns_achall.validation(self.dns_achall.account_key)) + http_expected = '{0}\n{1}\n{2}'.format( + self.http_achall.domain, self.http_achall.chall.encode('token'), + self.http_achall.validation(self.http_achall.account_key)) - self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls)) - self.assertEqual(2, mock_raw_input.call_count) - mock_verify.assert_called_with( - self.http01.challb.chall, "foo.com", KEY.public_key(), 8080) + self.assertEqual( + self.auth.perform(self.achalls), + [achall.response(achall.account_key) for achall in self.achalls]) + self.assertEqual( + self.auth.env[self.dns_achall.domain]['CERTBOT_AUTH_OUTPUT'], + dns_expected) + self.assertEqual( + self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'], + http_expected) - message = mock_stdout.write.mock_calls[0][1][0] - self.assertTrue(self.http01.chall.encode("token") in message) + @mock.patch('certbot.plugins.manual.zope.component.getUtility') + def test_manual_perform(self, mock_get_utility): + self.config.manual_public_ip_logging_ok = True + self.assertEqual( + self.auth.perform(self.achalls), + [achall.response(achall.account_key) for achall in self.achalls]) + for i, (args, kwargs) in enumerate(mock_get_utility().notification.call_args_list): + achall = self.achalls[i] + self.assertTrue(achall.validation(achall.account_key) in args[0]) + self.assertFalse(kwargs['wrap']) - mock_verify.return_value = False - with mock.patch("certbot.plugins.manual.logger") as mock_logger: - self.auth.perform(self.achalls) - self.assertEqual(2, mock_logger.warning.call_count) + def test_cleanup(self): + self.config.manual_public_ip_logging_ok = True + self.config.manual_auth_hook = 'echo foo;' + self.config.manual_cleanup_hook = '# cleanup' + self.auth.perform(self.achalls) - @mock.patch("certbot.plugins.manual.zope.component.getUtility") - @mock.patch("acme.challenges.DNS01Response.simple_verify") - @mock.patch("six.moves.input") - def test_perform_missing_dependency(self, mock_raw_input, mock_verify, mock_interaction): - mock_interaction().yesno.return_value = True - mock_verify.side_effect = acme_errors.DependencyError() + for achall in self.achalls: + self.auth.cleanup([achall]) + self.assertEqual(os.environ['CERTBOT_AUTH_OUTPUT'], 'foo') + self.assertEqual(os.environ['CERTBOT_DOMAIN'], achall.domain) + self.assertEqual( + os.environ['CERTBOT_VALIDATION'], + achall.validation(achall.account_key)) - with mock.patch("certbot.plugins.manual.logger") as mock_logger: - self.auth.perform([self.dns01]) - self.assertEqual(1, mock_logger.warning.call_count) - - mock_raw_input.assert_called_once_with("Press ENTER to continue") - - @mock.patch("certbot.plugins.manual.zope.component.getUtility") - @mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait") - def test_disagree_with_ip_logging(self, mock_notify, mock_interaction): - mock_interaction().yesno.return_value = False - mock_notify.side_effect = errors.Error("Exception not raised, \ - continued execution even after disagreeing with IP logging") - - self.assertRaises(errors.PluginError, self.auth.perform, self.achalls) - - @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True) - def test_perform_test_command_oserror(self, mock_popen): - mock_popen.side_effect = OSError - self.assertEqual([False], self.auth_test_mode.perform([self.http01])) - - @mock.patch("certbot.plugins.manual.socket.socket") - @mock.patch("certbot.plugins.manual.time.sleep", autospec=True) - @mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True) - def test_perform_test_command_run_failure( - self, mock_popen, unused_mock_sleep, unused_mock_socket): - mock_popen.poll.return_value = 10 - mock_popen.return_value.pid = 1234 - self.assertRaises( - errors.Error, self.auth_test_mode.perform, self.achalls) - - def test_cleanup_test_mode_already_terminated(self): - # pylint: disable=protected-access - self.auth_test_mode._httpd = httpd = mock.Mock() - httpd.poll.return_value = 0 - self.auth_test_mode.cleanup(self.achalls) - - def test_cleanup_test_mode_kills_still_running(self): - # pylint: disable=protected-access - self.auth_test_mode._httpd = httpd = mock.Mock(pid=1234) - httpd.poll.return_value = None - self.auth_test_mode.cleanup(self.achalls) - httpd.terminate.assert_called_once_with() + if isinstance(achall.chall, challenges.HTTP01): + self.assertEqual( + os.environ['CERTBOT_TOKEN'], + achall.chall.encode('token')) + else: + self.assertFalse('CERTBOT_TOKEN' in os.environ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/certbot/plugins/script.py b/certbot/plugins/script.py deleted file mode 100644 index 049ee8c96..000000000 --- a/certbot/plugins/script.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Script-based Authenticator.""" -import logging -import os -import sys - -import zope.interface - -from acme import challenges - -from certbot import errors -from certbot import interfaces -from certbot import hooks - -from certbot.plugins import common - -logger = logging.getLogger(__name__) - - -CHALLENGES = ["http-01", "dns-01"] - - -@zope.interface.implementer(interfaces.IAuthenticator) -@zope.interface.provider(interfaces.IPluginFactory) -class Authenticator(common.Plugin): - """Script authenticator - - calls user defined script to perform authentication and - optionally cleanup. - - """ - - description = "Authenticate using user provided script(s)" - - long_description = ("Authenticate using user provided script(s). " + - "Authenticator script has the following environment " + - "variables available for it: " + - "CERTBOT_DOMAIN - The domain being authenticated " + - "CERTBOT_VALIDATION - The validation string " + - "CERTBOT_TOKEN - Resource name part of HTTP-01 " + - "challenge (HTTP-01 only). " + - "Cleanup script has all the above, and additional " + - "var: CERTBOT_AUTH_OUTPUT - stdout output from the " + - "authenticator" - ) - - def __init__(self, *args, **kwargs): - super(Authenticator, self).__init__(*args, **kwargs) - self.cleanup_script = None - self.auth_script = None - self.challenges = [] - - @classmethod - def add_parser_arguments(cls, add): - add("auth", default=None, required=False, - help="path or command for the authentication script") - add("cleanup", default=None, required=False, - help="path or command for the cleanup script") - - @property - def supported_challenges(self): - """Challenges supported by this plugin.""" - return self.challenges - - def more_info(self): # pylint: disable=missing-docstring - return("This authenticator enables user to perform authentication " + - "using shell script(s).") - - def prepare(self): - """Prepare script plugin, check challenge, scripts and register them""" - pref_challenges = self.config.pref_challs - for c in pref_challenges: - if c.typ in CHALLENGES: - self.challenges.append(c) - if not self.challenges and len(pref_challenges): - # Challenges requested, but not supported - raise errors.PluginError( - "Unfortunately script plugin doesn't yet support " + - "the requested challenges") - - # Challenge not defined on cli, set default - if not self.challenges: - self.challenges.append(challenges.Challenge.TYPES["http-01"]) - - if not self.conf("auth"): - raise errors.PluginError("Parameter --script-auth is required " + - "for script plugin") - self._prepare_scripts() - - def _prepare_scripts(self): - """Helper method for prepare, to take care of validating scripts""" - script_path = self.conf("auth") - cleanup_path = self.conf("cleanup") - if self.config.validate_hooks: - hooks.validate_hook(script_path, "script_auth") - self.auth_script = script_path - if cleanup_path: - if self.config.validate_hooks: - hooks.validate_hook(cleanup_path, "script_cleanup") - self.cleanup_script = cleanup_path - - def get_chall_pref(self, domain): - """Return challenge(s) we're answering to """ - # pylint: disable=unused-argument - return self.challenges - - def perform(self, achalls): - """Perform the authentication per challenge""" - mapping = {"http-01": self._setup_env_http, - "dns-01": self._setup_env_dns} - responses = [] - for achall in achalls: - response, validation = achall.response_and_validation() - # Setup env vars - mapping[achall.typ](achall, validation) - output = self.execute(self.auth_script) - if output: - self._write_auth_output(output) - responses.append(response) - return responses - - def _setup_env_http(self, achall, validation): - """Write environment variables for http challenge""" - ev = dict() - ev["CERTBOT_TOKEN"] = achall.chall.encode("token") - ev["CERTBOT_VALIDATION"] = validation - ev["CERTBOT_DOMAIN"] = achall.domain - os.environ.update(ev) - - def _setup_env_dns(self, achall, validation): - """Write environment variables for dns challenge""" - ev = dict() - ev["CERTBOT_VALIDATION"] = validation - ev["CERTBOT_DOMAIN"] = achall.domain - os.environ.update(ev) - - def _write_auth_output(self, out): - """Write output from auth script to env var for - cleanup to act upon""" - os.environ.update({"CERTBOT_AUTH_OUTPUT": out.strip()}) - - def _normalize_string(self, value): - """Return string instead of bytestring for Python3. - Helper function for writing env vars, as os.environ needs str""" - - if isinstance(value, bytes): - value = value.decode(sys.getdefaultencoding()) - return str(value) - - def execute(self, shell_cmd): - """Run a script. - - :param str shell_cmd: Command to run - :returns: `str` stdout output""" - - _, out = hooks.execute(shell_cmd) - return self._normalize_string(out) - - def cleanup(self, achalls): # pylint: disable=unused-argument - """Run cleanup.sh """ - if self.cleanup_script: - self.execute(self.cleanup_script) diff --git a/certbot/plugins/script_test.py b/certbot/plugins/script_test.py deleted file mode 100644 index 1fe57a8dc..000000000 --- a/certbot/plugins/script_test.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Tests for certbot.plugins.manual.""" -import os -import tempfile -import unittest - -import mock - -from acme import challenges -from acme import jose - -from certbot import achallenges -from certbot import errors - -from certbot.tests import acme_util -from certbot.tests import util as test_util - - -KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) - - -class AuthenticatorTest(unittest.TestCase): - """Tests for certbot.plugins.script.Authenticator.""" - - def setUp(self): - from certbot.plugins.script import Authenticator - self.auth_return_value = "return from auth\n" - self.script_nonexec = create_script(b'# empty') - self.script_exec = create_script_exec(b'echo "return from auth\n"') - self.config = mock.MagicMock( - script_auth=self.script_exec, - script_cleanup=self.script_exec, - pref_challs=[challenges.Challenge.TYPES["http-01"], - challenges.Challenge.TYPES["dns-01"], - challenges.Challenge.TYPES["tls-sni-01"]]) - - self.tlssni_config = mock.MagicMock( - script_auth=self.script_exec, - script_cleanup=self.script_exec, - pref_challs=[challenges.Challenge.TYPES["tls-sni-01"]]) - - self.nochall_config = mock.MagicMock( - script_auth=self.script_exec, - script_cleanup=self.script_exec, - ) - - self.default = Authenticator(config=self.config, name="script") - self.onlytlssni = Authenticator(config=self.tlssni_config, - name="script") - self.nochall = Authenticator(config=self.nochall_config, - name="script") - - self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY) - self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY) - - self.achalls = [self.http01, self.dns01] - - def tearDown(self): - os.remove(self.script_exec) - os.remove(self.script_nonexec) - - def test_prepare_normal(self): - """Test prepare with typical configuration""" - from certbot.plugins.script import Authenticator - # Erroring combinations in from of (auth_script, cleanup_script, error) - for v in [("/NONEXISTENT/script.sh", "/NONEXISTENT/script.sh", - errors.HookCommandNotFound), - (self.script_nonexec, "/NONEXISTENT/script.sh", - errors.HookCommandNotFound), - (self.script_exec, "/NONEXISTENT/script.sh", - errors.HookCommandNotFound), - ("/NONEXISTENT/script.sh", self.script_nonexec, - errors.HookCommandNotFound), - ("/NONEXISTENT/script.sh", self.script_exec, - errors.HookCommandNotFound), - (None, self.script_exec, - errors.PluginError)]: - testconf = mock.MagicMock( - script_auth=v[0], - script_cleanup=v[1], - pref_challs=[challenges.Challenge.TYPES["http-01"]]) - testauth = Authenticator(config=testconf, name="script") - self.assertRaises(v[2], testauth.prepare) - - # This should not error - self.default.prepare() - self.assertEqual(len(self.default.challenges), 2) - - def test_prepare_tlssni(self): - """Test for provided, but unsupported challenge type""" - self.assertRaises(errors.PluginError, self.onlytlssni.prepare) - - def test_prepare_nochall(self): - """Test for default challenge""" - self.nochall.prepare() - self.assertEqual(len(self.nochall.challenges), 1) - - def test_more_info(self): - self.assertTrue(isinstance(self.default.more_info(), str)) - - def test_get_chall_pref(self): - self.default.prepare() - self.assertTrue(all(issubclass(pref, challenges.Challenge) - for pref in self.default.get_chall_pref( - "foo.com"))) - - def test_get_supported_challenges(self): - self.default.prepare() - self.assertTrue(all(issubclass(sup, challenges.Challenge) - for sup in self.default.supported_challenges)) - - def test_perform(self): - resp_http = self.http01.response(KEY) - resp_dns = self.dns01.response(KEY) - self.default.prepare() - # Check for the env vars prior to the run - self.assertFalse("CERTBOT_VALIDATION" in os.environ.keys()) - self.assertFalse("CERTBOT_DOMAIN" in os.environ.keys()) - self.assertFalse("CERTBOT_AUTH_OUTPUT" in os.environ.keys()) - - pref_resp = self.default.perform(self.achalls) - self.assertEqual([resp_http, resp_dns], pref_resp) - # Check for the env vars post run - self.assertTrue("CERTBOT_VALIDATION" in os.environ.keys()) - self.assertTrue("CERTBOT_DOMAIN" in os.environ.keys()) - self.assertTrue("CERTBOT_AUTH_OUTPUT" in os.environ.keys()) - self.assertEqual(os.environ["CERTBOT_AUTH_OUTPUT"], - self.auth_return_value.strip()) - - @mock.patch('certbot.plugins.script.Authenticator.execute') - def test_cleanup(self, mock_exec): - mock_exec.return_value = (0, None, None) - self.default.prepare() - self.default.cleanup(self.achalls) - self.assertEqual(mock_exec.call_count, 1) - - @mock.patch('certbot.hooks.Popen') - def test_execute(self, mock_popen): - proc = mock.Mock() - # tuple values: stdout, stderr, errorcode, num_of_logger_calls - for t in [("", "", 0, 0), - (self.auth_return_value, "", 0, 0), - (None, "stderr_output", 0, 1), - ("whatever", "stderr_output", 1, 2), - (b'bytestring outval', "", 0, 0)]: - proc = mock.Mock() - attrs = {'communicate.return_value': (t[0], t[1]), - 'returncode': t[2]} - proc.configure_mock(**attrs) # pylint: disable=star-args - mock_popen.return_value = proc - with mock.patch('certbot.hooks.logger.error') as mock_log: - output = self.default.execute(self.script_exec) - self.assertEqual(mock_log.call_count, t[3]) - self.assertTrue(isinstance(output, str)) - - -def create_script(contents): - """ Helper to create temporary file """ - f = tempfile.NamedTemporaryFile(delete=False, prefix='.sh') - f.write(contents) - f.close() - return f.name - - -def create_script_exec(contents): - """ Helper to create temporary file with exec permissions""" - fname = create_script(contents) - os.chmod(fname, 0o700) - return fname diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 16932232a..81387c435 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -133,7 +133,7 @@ def choose_plugin(prepared, question): else: return None -noninstaller_plugins = ["webroot", "manual", "standalone", "script"] +noninstaller_plugins = ["webroot", "manual", "standalone"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -238,8 +238,6 @@ def cli_plugin_requests(config): req_auth = set_configurator(req_auth, "webroot") if config.manual: req_auth = set_configurator(req_auth, "manual") - if config.script: - req_auth = set_configurator(req_auth, "script") logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) return req_auth, req_inst diff --git a/certbot/tests/acme_util.py b/certbot/tests/acme_util.py index 3168349c9..5e6b190a7 100644 --- a/certbot/tests/acme_util.py +++ b/certbot/tests/acme_util.py @@ -7,9 +7,12 @@ from acme import challenges from acme import jose from acme import messages +from certbot import auth_handler + from certbot.tests import util +JWK = jose.JWK.load(util.load_vector('rsa512_key.pem')) KEY = util.load_rsa_private_key('rsa512_key.pem') # Challenges @@ -50,6 +53,14 @@ DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING) CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P] +# AnnotatedChallenge objects +HTTP01_A = auth_handler.challb_to_achall(HTTP01_P, JWK, "example.com") +TLSSNI01_A = auth_handler.challb_to_achall(TLSSNI01_P, JWK, "example.net") +DNS01_A = auth_handler.challb_to_achall(DNS01_P, JWK, "example.org") + +ACHALLENGES = [HTTP01_A, TLSSNI01_A, DNS01_A] + + def gen_authzr(authz_status, domain, challs, statuses, combos=True): """Generate an authorization resource. diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index e3bd28a5e..9404a8385 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -81,7 +81,7 @@ class ParseTest(unittest.TestCase): out = self._help_output(['--help', 'all']) self.assertTrue("--configurator" in out) self.assertTrue("how a cert is deployed" in out) - self.assertTrue("--manual-test-mode" in out) + self.assertTrue("--webroot-path" in out) self.assertTrue("--text" not in out) self.assertTrue("--dialog" not in out) self.assertTrue("%s" not in out) @@ -91,7 +91,7 @@ class ParseTest(unittest.TestCase): if "nginx" in self.plugins: # may be false while building distributions without plugins self.assertTrue("--nginx-ctl" in out) - self.assertTrue("--manual-test-mode" not in out) + self.assertTrue("--webroot-path" not in out) self.assertTrue("--checkpoints" not in out) out = self._help_output(['-h']) @@ -102,7 +102,7 @@ class ParseTest(unittest.TestCase): self.assertTrue("(the certbot nginx plugin is not" in out) out = self._help_output(['--help', 'plugins']) - self.assertTrue("--manual-test-mode" not in out) + self.assertTrue("--webroot-path" not in out) self.assertTrue("--prepare" in out) self.assertTrue('"plugins" subcommand' in out) @@ -305,22 +305,22 @@ class SetByCliTest(unittest.TestCase): def test_report_config_interaction_str(self): cli.report_config_interaction('manual_public_ip_logging_ok', - 'manual_test_mode') - cli.report_config_interaction('manual_test_mode', 'manual') + 'manual_auth_hook') + cli.report_config_interaction('manual_auth_hook', 'manual') self._test_report_config_interaction_common() def test_report_config_interaction_iterable(self): cli.report_config_interaction(('manual_public_ip_logging_ok',), - ('manual_test_mode',)) - cli.report_config_interaction(('manual_test_mode',), ('manual',)) + ('manual_auth_hook',)) + cli.report_config_interaction(('manual_auth_hook',), ('manual',)) self._test_report_config_interaction_common() def _test_report_config_interaction_common(self): """Tests implied interaction between manual flags. - --manual implies --manual-test-mode which implies + --manual implies --manual-auth-hook which implies --manual-public-ip-logging-ok. These interactions don't actually exist in the client, but are used here for testing purposes. @@ -328,13 +328,13 @@ class SetByCliTest(unittest.TestCase): args = ['--manual'] verb = 'renew' - for v in ('manual', 'manual_test_mode', 'manual_public_ip_logging_ok'): + for v in ('manual', 'manual_auth_hook', 'manual_public_ip_logging_ok'): self.assertTrue(_call_set_by_cli(v, args, verb)) cli.set_by_cli.detector = None - args = ['--manual-test-mode'] - for v in ('manual_test_mode', 'manual_public_ip_logging_ok'): + args = ['--manual-auth-hook', 'command'] + for v in ('manual_auth_hook', 'manual_public_ip_logging_ok'): self.assertTrue(_call_set_by_cli(v, args, verb)) self.assertFalse(_call_set_by_cli('manual', args, verb)) diff --git a/setup.py b/setup.py index 46dbdac81..4227d5d92 100644 --- a/setup.py +++ b/setup.py @@ -131,7 +131,6 @@ setup( 'null = certbot.plugins.null:Installer', 'standalone = certbot.plugins.standalone:Authenticator', 'webroot = certbot.plugins.webroot:Authenticator', - 'script = certbot.plugins.script:Authenticator', ], }, ) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index a70f13f8e..e7975454b 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -44,7 +44,12 @@ python_server_pid=$! common --domains le2.wtf --preferred-challenges http-01 run kill $python_server_pid -common -a manual -d le.wtf auth --rsa-key-size 4096 +common certonly -a manual -d le.wtf --rsa-key-size 4096 \ + --manual-auth-hook ./tests/manual-http-auth.sh \ + --manual-cleanup-hook ./tests/manual-http-cleanup.sh + +common certonly -a manual -d dns.le.wtf --preferred-challenges dns-01 \ + --manual-auth-hook ./tests/manual-dns-auth.sh export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ OPENSSL_CNF=examples/openssl.cnf diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 8d01ad763..12924fe21 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -25,7 +25,7 @@ certbot_test_no_force_renew () { --no-verify-ssl \ --tls-sni-01-port $tls_sni_01_port \ --http-01-port $http_01_port \ - --manual-test-mode \ + --manual-public-ip-logging-ok \ $store_flags \ --non-interactive \ --no-redirect \ diff --git a/tests/manual-dns-auth.sh b/tests/manual-dns-auth.sh new file mode 100755 index 000000000..9b9a1a5eb --- /dev/null +++ b/tests/manual-dns-auth.sh @@ -0,0 +1,4 @@ +#!/bin/sh +curl -X POST 'http://localhost:8055/set-txt' -d \ + "{\"host\": \"_acme-challenge.$CERTBOT_DOMAIN.\", \ + \"value\": \"$CERTBOT_VALIDATION\"}" diff --git a/tests/manual-http-auth.sh b/tests/manual-http-auth.sh new file mode 100755 index 000000000..c4730392b --- /dev/null +++ b/tests/manual-http-auth.sh @@ -0,0 +1,12 @@ +#!/bin/sh +uri_path=".well-known/acme-challenge/$CERTBOT_TOKEN" + +cd $(mktemp -d) +mkdir -p $(dirname $uri_path) +echo $CERTBOT_VALIDATION > $uri_path +python -m SimpleHTTPServer $http_01_port >/dev/null 2>&1 & +server_pid=$! +while ! curl "http://localhost:$http_01_port/$uri_path" >/dev/null 2>&1; do + sleep 1s +done +echo $server_pid diff --git a/tests/manual-http-cleanup.sh b/tests/manual-http-cleanup.sh new file mode 100755 index 000000000..5e437bf08 --- /dev/null +++ b/tests/manual-http-cleanup.sh @@ -0,0 +1,2 @@ +#!/bin/sh +kill $CERTBOT_AUTH_OUTPUT From 19143d83036cdcbfc98c16f202a207c888ce18e3 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 22 Dec 2016 13:07:00 -0800 Subject: [PATCH 330/331] Increase test coverage --- certbot/tests/cert_manager_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 6caaab878..07f7cedaa 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -181,8 +181,8 @@ class CertificatesTest(BaseCertManagerTest): shutil.rmtree(tempdir) @mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked') - def test_report_human_readable(self, mock_ocsp): - mock_ocsp.return_value = None + def test_report_human_readable(self, mock_revoked): + mock_revoked.return_value = None from certbot import cert_manager import datetime, pytz expiry = pytz.UTC.fromutc(datetime.datetime.utcnow()) @@ -221,8 +221,9 @@ class CertificatesTest(BaseCertManagerTest): self.assertTrue('VALID' in out and not 'INVALID' in out) cert.is_test_cert = True + mock_revoked.return_value = True out = get_report() - self.assertTrue('INVALID: TEST_CERT' in out) + self.assertTrue('INVALID: TEST_CERT, REVOKED' in out) cert = mock.MagicMock(lineagename="indescribable") cert.target_expiry = expiry From 9aa93c05c1a4b88ab939660737db3d5e70d1b86d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 22 Dec 2016 15:35:29 -0800 Subject: [PATCH 331/331] Simplify the ocsp_revoked() return type - we weren't reacting to None, so call it False instead --- certbot/ocsp.py | 12 ++++++------ certbot/tests/ocsp_test.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/certbot/ocsp.py b/certbot/ocsp.py index a5df5743d..2e0514a44 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -37,17 +37,17 @@ class RevocationChecker(object): :param str cert_path: Path to certificate :param str chain_path: Path to intermediate cert :rtype bool or None: - :returns: False if valid; True if revoked; None if check itself failed + :returns: True if revoked; False if valid or the check failed """ if self.broken: - return None + return False logger.debug("Querying OCSP for %s", cert_path) url, host = self.determine_ocsp_server(cert_path) if not host: - return None + return False # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this! cmd = ["openssl", "ocsp", "-no_nonce", @@ -62,7 +62,7 @@ class RevocationChecker(object): except errors.SubprocessError as e: logger.info("OCSP check failed for %s (are we offline?)", cert_path) logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), e) - return None + return False return _translate_ocsp_query(cert_path, output, err) @@ -98,12 +98,12 @@ def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors): if not "Response verify OK" in ocsp_errors: logger.info("Revocation status for %s is unknown", cert_path) logger.debug("Uncertain ouput:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors) - return None + return False if cert_path + ": good" in ocsp_output: return False elif cert_path + ": revoked" in ocsp_output: return True else: logger.warn("Unable to properly parse OCSP output: %s", ocsp_output) - return None + return False diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index c4517174d..ff79bb01e 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -59,17 +59,17 @@ class OCSPTest(unittest.TestCase): def test_ocsp_revoked(self, mock_run, mock_determine): self.checker.broken = True mock_determine.return_value = ("", "") - self.assertEqual(self.checker.ocsp_revoked("x", "y"), None) + self.assertEqual(self.checker.ocsp_revoked("x", "y"), False) self.checker.broken = False mock_run.return_value = tuple(openssl_happy[1:]) - self.assertEqual(self.checker.ocsp_revoked("x", "y"), None) + self.assertEqual(self.checker.ocsp_revoked("x", "y"), False) self.assertEqual(mock_run.call_count, 0) mock_determine.return_value = ("http://x.co", "x.co") self.assertEqual(self.checker.ocsp_revoked("blah.pem", "chain.pem"), False) mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher") - self.assertEqual(self.checker.ocsp_revoked("x", "y"), None) + self.assertEqual(self.checker.ocsp_revoked("x", "y"), False) self.assertEqual(mock_run.call_count, 2) @@ -97,10 +97,10 @@ class OCSPTest(unittest.TestCase): mock_run.return_value = openssl_confused from certbot import ocsp self.assertEqual(ocsp._translate_ocsp_query(*openssl_happy), False) - self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), None) + self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), False) self.assertEqual(mock_log.debug.call_count, 1) self.assertEqual(mock_log.warn.call_count, 0) - self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), None) + self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), False) self.assertEqual(mock_log.warn.call_count, 1) self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True)