From 44f4743b515e62a0f63436ddd990b90b059ab990 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Tue, 23 May 2017 16:25:39 -0700 Subject: [PATCH 001/125] Mechanism for automatically updating options-ssl-apache.conf file * add file update mechanism + tests to apache * update with actual hashes, and update apache test to match since there aren't previous versions --- certbot-apache/certbot_apache/configurator.py | 26 +++--- certbot-apache/certbot_apache/constants.py | 10 +++ .../certbot_apache/tests/configurator_test.py | 83 ++++++++++++++++++- 3 files changed, 103 insertions(+), 16 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 13b325d7f..24a2f3934 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -5,7 +5,6 @@ import fnmatch import logging import os import re -import shutil import socket import time @@ -145,6 +144,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) + @property + def updated_mod_ssl_conf_digest(self): + """Full absolute path to digest of updated SSL configuration file.""" + return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST) + def prepare(self): """Prepare the authenticator/installer. @@ -195,7 +199,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Get all of the available vhosts self.vhosts = self.get_virtual_hosts() - install_ssl_options_conf(self.mod_ssl_conf) + install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest) # Prevent two Apache plugins from modifying a config at once try: @@ -1981,19 +1985,11 @@ def _split_aug_path(vhost_path): return file_path, "/".join(reversed(internal_path)) -def install_ssl_options_conf(options_ssl): - """ - Copy Certbot's SSL options file into the system's config dir if - required. - """ +def install_ssl_options_conf(options_ssl, options_ssl_digest): + """Copy Certbot's SSL options file into the system's config dir if required.""" + # XXX if we ever try to enforce a local privilege boundary (eg, running # certbot for unprivileged users via setuid), this function will need # to be modified. - - # XXX if the user is in security-autoupdate mode, we should be willing to - # overwrite the options_ssl file at least if it's unmodified: - # https://github.com/letsencrypt/letsencrypt/issues/1123 - - # Check to make sure options-ssl.conf is installed - if not os.path.isfile(options_ssl): - shutil.copyfile(constants.os_constant("MOD_SSL_CONF_SRC"), options_ssl) + return common.install_ssl_options_conf(options_ssl, options_ssl_digest, + constants.os_constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES, logger) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index 3cfeb4dd6..0b96d392e 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -131,6 +131,16 @@ CLI_DEFAULTS = { MOD_SSL_CONF_DEST = "options-ssl-apache.conf" """Name of the mod_ssl config file as saved in `IConfig.config_dir`.""" + +UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-apache-conf-digest.txt" +"""Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`.""" + +ALL_SSL_OPTIONS_HASHES = [ + '2086bca02db48daf93468332543c60ac6acdb6f0b58c7bfdf578a5d47092f82a', + '4844d36c9a0f587172d9fa10f4f1c9518e3bcfa1947379f155e16a70a728c21a', +] +"""SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC""" + AUGEAS_LENS_DIR = pkg_resources.resource_filename( "certbot_apache", "augeas_lens") """Path to the Augeas lens directory""" diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 75589dce5..055feeece 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -12,6 +12,7 @@ import six # pylint: disable=unused-import from acme import challenges from certbot import achallenges +from certbot import crypto_util from certbot import errors from certbot.tests import acme_util @@ -826,8 +827,10 @@ class MultipleVhostsTest(util.ApacheTest): def test_install_ssl_options_conf(self): from certbot_apache.configurator import install_ssl_options_conf path = os.path.join(self.work_dir, "test_it") - install_ssl_options_conf(path) + other_path = os.path.join(self.work_dir, "other_test_it") + install_ssl_options_conf(path, other_path) self.assertTrue(os.path.isfile(path)) + self.assertTrue(os.path.isfile(other_path)) # TEST ENHANCEMENTS def test_supported_enhancements(self): @@ -1432,5 +1435,83 @@ class MultiVhostsTest(util.ApacheTest): mock.ANY) +class InstallSslOptionsConfTest(util.ApacheTest): + """Test that the options-ssl-nginx.conf file is installed and updated properly.""" + + def setUp(self): # pylint: disable=arguments-differ + super(InstallSslOptionsConfTest, self).setUp() + + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir) + + def _call(self): + from certbot_apache.configurator import install_ssl_options_conf + install_ssl_options_conf(self.config.mod_ssl_conf, self.config.updated_mod_ssl_conf_digest) + + def _current_ssl_options_hash(self): + return crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")) + + def _assert_current_file(self): + self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) + self.assertEqual(crypto_util.sha256sum(self.config.mod_ssl_conf), + self._current_ssl_options_hash()) + + def test_no_file(self): + # prepare should have placed a file there + self._assert_current_file() + os.remove(self.config.mod_ssl_conf) + self.assertFalse(os.path.isfile(self.config.mod_ssl_conf)) + self._call() + self._assert_current_file() + + def test_current_file(self): + self._assert_current_file() + self._call() + self._assert_current_file() + + def test_prev_file_updates_to_current(self): + from certbot_apache.constants import ALL_SSL_OPTIONS_HASHES + ALL_SSL_OPTIONS_HASHES.insert(0, "test_hash_does_not_match") + with mock.patch('certbot.crypto_util.sha256sum') as mock_sha256: + mock_sha256.return_value = ALL_SSL_OPTIONS_HASHES[0] + self._call() + self._assert_current_file() + + def test_manually_modified_current_file_does_not_update(self): + with open(self.config.mod_ssl_conf, "a") as mod_ssl_conf: + mod_ssl_conf.write("a new line for the wrong hash\n") + with mock.patch("certbot_apache.configurator.logger") as mock_logger: + self._call() + self.assertFalse(mock_logger.warning.called) + self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) + self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), + self._current_ssl_options_hash()) + self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf), + self._current_ssl_options_hash()) + + def test_manually_modified_past_file_warns(self): + with open(self.config.mod_ssl_conf, "a") as mod_ssl_conf: + mod_ssl_conf.write("a new line for the wrong hash\n") + with open(self.config.updated_mod_ssl_conf_digest, "w") as f: + f.write("hashofanoldversion") + with mock.patch("certbot_apache.configurator.logger") as mock_logger: + self._call() + self.assertEqual(mock_logger.warning.call_args[0][0], + "%s has been manually modified; updated ssl configuration options " + "saved to %s. We recommend updating %s for security purposes.") + self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), + self._current_ssl_options_hash()) + # only print warning once + with mock.patch("certbot_apache.configurator.logger") as mock_logger: + self._call() + self.assertFalse(mock_logger.warning.called) + + def test_current_file_hash_in_all_hashes(self): + from certbot_apache.constants import ALL_SSL_OPTIONS_HASHES + self.assertTrue(self._current_ssl_options_hash() in ALL_SSL_OPTIONS_HASHES, + "Constants.ALL_SSL_OPTIONS_HASHES must be appended" + " with the sha256 hash of self.config.mod_ssl_conf when it is updated.") + + if __name__ == "__main__": unittest.main() # pragma: no cover From 844c2d34389ebe104979a10b3715e127627c8aeb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 1 Jun 2017 09:12:50 -0700 Subject: [PATCH 002/125] Finish work on #4718. * Update in response to changes in #4720. * Update ALL_SSL_OPTIONS_HASHES. * Add warning to Apache's SSL options files. --- .../certbot_apache/centos-options-ssl-apache.conf | 6 +++++- certbot-apache/certbot_apache/configurator.py | 2 +- certbot-apache/certbot_apache/constants.py | 4 ++++ certbot-apache/certbot_apache/options-ssl-apache.conf | 6 +++++- certbot-apache/certbot_apache/tests/configurator_test.py | 6 +++--- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/certbot-apache/certbot_apache/centos-options-ssl-apache.conf b/certbot-apache/certbot_apache/centos-options-ssl-apache.conf index fbe8da0f2..17ae1be76 100644 --- a/certbot-apache/certbot_apache/centos-options-ssl-apache.conf +++ b/certbot-apache/certbot_apache/centos-options-ssl-apache.conf @@ -1,4 +1,8 @@ -# Baseline setting to Include for SSL sites +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. SSLEngine on diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 24a2f3934..097c4ff42 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -1992,4 +1992,4 @@ def install_ssl_options_conf(options_ssl, options_ssl_digest): # certbot for unprivileged users via setuid), this function will need # to be modified. return common.install_ssl_options_conf(options_ssl, options_ssl_digest, - constants.os_constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES, logger) + constants.os_constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index 0b96d392e..7aced5dca 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -138,6 +138,10 @@ UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-apache-conf-digest.txt" ALL_SSL_OPTIONS_HASHES = [ '2086bca02db48daf93468332543c60ac6acdb6f0b58c7bfdf578a5d47092f82a', '4844d36c9a0f587172d9fa10f4f1c9518e3bcfa1947379f155e16a70a728c21a', + '5a922826719981c0a234b1fbcd495f3213e49d2519e845ea0748ba513044b65b', + '4066b90268c03c9ba0201068eaa39abbc02acf9558bb45a788b630eb85dadf27', + 'f175e2e7c673bd88d0aff8220735f385f916142c44aa83b09f1df88dd4767a88', + 'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b', ] """SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC""" diff --git a/certbot-apache/certbot_apache/options-ssl-apache.conf b/certbot-apache/certbot_apache/options-ssl-apache.conf index ec07a4ba3..950a02a8b 100644 --- a/certbot-apache/certbot_apache/options-ssl-apache.conf +++ b/certbot-apache/certbot_apache/options-ssl-apache.conf @@ -1,4 +1,8 @@ -# Baseline setting to Include for SSL sites +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. SSLEngine on diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 055feeece..db04bfcd1 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1480,7 +1480,7 @@ class InstallSslOptionsConfTest(util.ApacheTest): def test_manually_modified_current_file_does_not_update(self): with open(self.config.mod_ssl_conf, "a") as mod_ssl_conf: mod_ssl_conf.write("a new line for the wrong hash\n") - with mock.patch("certbot_apache.configurator.logger") as mock_logger: + with mock.patch("certbot.plugins.common.logger") as mock_logger: self._call() self.assertFalse(mock_logger.warning.called) self.assertTrue(os.path.isfile(self.config.mod_ssl_conf)) @@ -1494,7 +1494,7 @@ class InstallSslOptionsConfTest(util.ApacheTest): mod_ssl_conf.write("a new line for the wrong hash\n") with open(self.config.updated_mod_ssl_conf_digest, "w") as f: f.write("hashofanoldversion") - with mock.patch("certbot_apache.configurator.logger") as mock_logger: + with mock.patch("certbot.plugins.common.logger") as mock_logger: self._call() self.assertEqual(mock_logger.warning.call_args[0][0], "%s has been manually modified; updated ssl configuration options " @@ -1502,7 +1502,7 @@ class InstallSslOptionsConfTest(util.ApacheTest): self.assertEqual(crypto_util.sha256sum(constants.os_constant("MOD_SSL_CONF_SRC")), self._current_ssl_options_hash()) # only print warning once - with mock.patch("certbot_apache.configurator.logger") as mock_logger: + with mock.patch("certbot.plugins.common.logger") as mock_logger: self._call() self.assertFalse(mock_logger.warning.called) From d25069d89bdbf303d1139d854b00e94df754ebc6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 1 Jun 2017 15:26:54 -0700 Subject: [PATCH 003/125] Remove reference to .new in Nginx's SSL options. (#4769) --- certbot-nginx/certbot_nginx/constants.py | 1 + certbot-nginx/certbot_nginx/options-ssl-nginx.conf | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/constants.py b/certbot-nginx/certbot_nginx/constants.py index a74f97662..2e72b8686 100644 --- a/certbot-nginx/certbot_nginx/constants.py +++ b/certbot-nginx/certbot_nginx/constants.py @@ -27,6 +27,7 @@ ALL_SSL_OPTIONS_HASHES = [ 'a6d9f1c7d6b36749b52ba061fff1421f9a0a3d2cfdafbd63c05d06f65b990937', '7f95624dd95cf5afc708b9f967ee83a24b8025dc7c8d9df2b556bbc64256b3ff', '394732f2bbe3e5e637c3fb5c6e980a1f1b90b01e2e8d6b7cff41dde16e2a756d', + '4b16fec2bcbcd8a2f3296d886f17f9953ffdcc0af54582452ca1e52f5f776f16', ] """SHA256 hashes of the contents of all versions of MOD_SSL_CONF_SRC""" diff --git a/certbot-nginx/certbot_nginx/options-ssl-nginx.conf b/certbot-nginx/certbot_nginx/options-ssl-nginx.conf index 7303f9bc6..292d42984 100644 --- a/certbot-nginx/certbot_nginx/options-ssl-nginx.conf +++ b/certbot-nginx/certbot_nginx/options-ssl-nginx.conf @@ -1,7 +1,8 @@ -# This file contains important security parameters. If you modify this file manually, -# Certbot will be unable to automatically provide future security updates. -# Instead, you will need to manually update this file by referencing the contents of -# options-ssl-nginx.conf.new. +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. ssl_session_cache shared:le_nginx_SSL:1m; ssl_session_timeout 1440m; From 6ee934b667e124925029c0ef83de30d1302c5bc9 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Mon, 5 Jun 2017 11:44:22 -0700 Subject: [PATCH 004/125] route53: shorten description to one line (#4772) The new description is less informative than the current one, but its shorter length makes the interactive plugin selection prompt easier to read. --- certbot-route53/certbot_route53/authenticator.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/certbot-route53/certbot_route53/authenticator.py b/certbot-route53/certbot_route53/authenticator.py index d53fd102f..74bf9b283 100644 --- a/certbot-route53/certbot_route53/authenticator.py +++ b/certbot-route53/certbot_route53/authenticator.py @@ -27,11 +27,7 @@ class Authenticator(common.Plugin): This authenticator solves a DNS01 challenge by uploading the answer to AWS Route53. """ - - description = ("Authenticate domain names using the DNS challenge type, " - "by automatically updating TXT records using AWS Route53. Works only " - "if you use AWS Route53 to host DNS for your domains. " + - INSTRUCTIONS) + description = "Obtain certs using a DNS TXT record (if you are using AWS Route53 for DNS)." def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) From 0e4f55982acf173c3511e7b3aac04d2986f893e7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 5 Jun 2017 15:20:04 -0700 Subject: [PATCH 005/125] Remove unused readlink environment variable. (#4781) The program readlink used to be used in integration tests so an environment variable was used to handle differences in the executable on different systems. This command is no longer used though so the variable can be removed. --- tests/boulder-integration.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 5c00be054..ddbaa43ed 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -13,12 +13,6 @@ set -eux . ./tests/integration/_common.sh export PATH="$PATH:/usr/sbin" # /usr/sbin/nginx -if [ `uname` = "Darwin" ];then - readlink="greadlink" -else - readlink="readlink" -fi - cleanup_and_exit() { EXIT_STATUS=$? if SERVER_STILL_RUNNING=`ps -p $python_server_pid -o pid=` From 2325438b56df01601ff07b14b7be24439d7654ae Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Mon, 5 Jun 2017 17:09:03 -0700 Subject: [PATCH 006/125] route53: fix error handling (#4760) Make error handling match other plugins: * Raise `PluginError` instead of errors from underlying libraries * Swallow errors during cleanup --- .../certbot_route53/authenticator.py | 14 +++++++++----- .../certbot_route53/authenticator_test.py | 18 +++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/certbot-route53/certbot_route53/authenticator.py b/certbot-route53/certbot_route53/authenticator.py index 74bf9b283..96b4d1abb 100644 --- a/certbot-route53/certbot_route53/authenticator.py +++ b/certbot-route53/certbot_route53/authenticator.py @@ -7,6 +7,7 @@ import zope.interface from acme import challenges from botocore.exceptions import NoCredentialsError, ClientError +from certbot import errors from certbot import interfaces from certbot.plugins import common @@ -59,12 +60,15 @@ class Authenticator(common.Plugin): time.sleep(TTL) return [achall.response(achall.account_key) for achall in achalls] except (NoCredentialsError, ClientError) as e: - e.args = ("\n".join([str(e), INSTRUCTIONS]),) - raise + logger.debug('Encountered error during perform: %s', e, exc_info=True) + raise errors.PluginError("\n".join([str(e), INSTRUCTIONS])) def cleanup(self, achalls): # pylint: disable=missing-docstring for achall in achalls: - self._change_txt_record("DELETE", achall) + try: + self._change_txt_record("DELETE", achall) + except (NoCredentialsError, ClientError) as e: + logger.debug('Encountered error during cleanup: %s', e, exc_info=True) def _find_zone_id_for_domain(self, domain): """Find the zone id responsible a given FQDN. @@ -85,7 +89,7 @@ class Authenticator(common.Plugin): zones.append((zone["Name"], zone["Id"])) if not zones: - raise ValueError( + raise errors.PluginError( "Unable to find a Route53 hosted zone for {0}".format(domain) ) @@ -134,6 +138,6 @@ class Authenticator(common.Plugin): if response["ChangeInfo"]["Status"] == "INSYNC": return time.sleep(5) - raise Exception( + raise errors.PluginError( "Timed out waiting for Route53 change. Current status: %s" % response["ChangeInfo"]["Status"]) diff --git a/certbot-route53/certbot_route53/authenticator_test.py b/certbot-route53/certbot_route53/authenticator_test.py index 545fd01a4..f16224d84 100644 --- a/certbot-route53/certbot_route53/authenticator_test.py +++ b/certbot-route53/certbot_route53/authenticator_test.py @@ -3,9 +3,9 @@ import unittest import mock - from botocore.exceptions import NoCredentialsError, ClientError +from certbot import errors from certbot.plugins import dns_test_common @@ -36,7 +36,7 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest def test_perform_no_credentials_error(self): self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) - self.assertRaises(NoCredentialsError, # TODO: Should be `errors.PluginError` + self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) @@ -44,7 +44,7 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest self.auth._change_txt_record = mock.MagicMock( side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) - self.assertRaises(ClientError, # TODO: Should be `errors.PluginError` + self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) @@ -58,17 +58,13 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest def test_cleanup_no_credentials_error(self): self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) - self.assertRaises(NoCredentialsError, # TODO: Should not raise - self.auth.cleanup, - [self.achall]) + self.auth.cleanup([self.achall]) def test_cleanup_client_error(self): self.auth._change_txt_record = mock.MagicMock( side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) - self.assertRaises(ClientError, # TODO: Should not raise - self.auth.cleanup, - [self.achall]) + self.auth.cleanup([self.achall]) class ClientTest(unittest.TestCase): @@ -153,7 +149,7 @@ class ClientTest(unittest.TestCase): self.client.r53.get_paginator = mock.MagicMock() self.client.r53.get_paginator().paginate.return_value = [] - self.assertRaises(ValueError, # TODO: Should be `errors.PluginError` + self.assertRaises(errors.PluginError, self.client._find_zone_id_for_domain, "foo.example.com") @@ -168,7 +164,7 @@ class ClientTest(unittest.TestCase): }, ] - self.assertRaises(ValueError, # TODO: Should be `errors.PluginError` + self.assertRaises(errors.PluginError, self.client._find_zone_id_for_domain, "foo.example.com") From 962879c35c169b44a53fabb0c9490c1a89ccf193 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 5 Jun 2017 17:51:45 -0700 Subject: [PATCH 007/125] Remove dependency on git from pip_install.sh. (#4770) * Remove dependency on git from pip_install.sh. Using git allowed this file to continue to work even if it was moved to another directory. This slight increase in robustness wasn't worth it though as it broke our development Dockerfile (see #4703), the certbot website's Dockerfile (see certbot/website#226), and our test farm tests (see certbot/tests/letstest/scripts/test_apache2.sh for an example that calls tools/venv.sh without installing git). Rather than continuing to find and patch these things, let's just allow this script to fail if it's moved rather than propagating the git dependency all over the place. * Add readlink.py. This is the equivalent of `readlink -f` on many Linux systems. This is useful as there are often differences in readlink on different platforms. * Use readlink.py in pip_install.sh. This allows us to work around differences in readlink on macOS. --- tools/pip_install.sh | 3 ++- tools/readlink.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100755 tools/readlink.py diff --git a/tools/pip_install.sh b/tools/pip_install.sh index 8a58f9e48..438e567e4 100755 --- a/tools/pip_install.sh +++ b/tools/pip_install.sh @@ -2,7 +2,8 @@ # pip installs packages using Certbot's requirements file as constraints # get the root of the Certbot repo -repo_root=$(git rev-parse --show-toplevel) +my_path=$("$(dirname $0)/readlink.py" $0) +repo_root=$(dirname $(dirname $my_path)) requirements="$repo_root/letsencrypt-auto-source/pieces/dependency-requirements.txt" constraints=$(mktemp) trap "rm -f $constraints" EXIT diff --git a/tools/readlink.py b/tools/readlink.py new file mode 100755 index 000000000..02c74c48d --- /dev/null +++ b/tools/readlink.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +"""Canonicalizes a path and follows any symlinks. + +This is the equivalent of `readlink -f` on many Linux systems. This is +useful as there are often differences in readlink on different +platforms. + +""" +from __future__ import print_function +import os +import sys + +print(os.path.realpath(sys.argv[1])) From 4448a860138b41711864c56ae46bc729a24843ba Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Mon, 5 Jun 2017 17:59:56 -0700 Subject: [PATCH 008/125] Handle releasing of DNS subpackages not yet included in certbot-auto (#4779) Add the DNS subpackages being considered for future inclusion in certbot-auto as non-certbot-auto packages for the 0.15.0 release. --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index 3807cea32..ef15f1828 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,7 +46,7 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudxns certbot-dns-dnsimple certbot-dns-nsone" +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-google certbot-dns-nsone certbot-dns-route53" # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" From 89e63eaf84918852598160e4a0d0175cb134999e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 5 Jun 2017 18:21:47 -0700 Subject: [PATCH 009/125] Use certificate in NS1 plugin flag's help. (#4783) --- certbot/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index 1023d9aca..78e4032f1 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1247,7 +1247,8 @@ def _plugins_parsing(helpful, plugins): help=('Obtain certificates using a DNS TXT record (if you are ' 'using Google Cloud DNS).')) helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true", - help='Obtain certs using a DNS TXT record (if you are using NS1 for DNS).') + help=('Obtain certificates using a DNS TXT record (if you are ' + 'using NS1 for DNS).')) # things should not be reorder past/pre this comment: # plugins_group should be displayed in --help before plugin From a926d42bd6f3f922928eb11ad5dbcab2f6f9f7af Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Mon, 5 Jun 2017 19:06:35 -0700 Subject: [PATCH 010/125] Use certificate instead of cert in DNS plugin descriptions (#4784) --- .../certbot_dns_cloudflare/dns_cloudflare.py | 3 ++- certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py | 2 +- certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py | 2 +- certbot-dns-google/certbot_dns_google/dns_google.py | 3 ++- certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py | 2 +- certbot-route53/certbot_route53/authenticator.py | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py index 6979581ee..f1156642f 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py @@ -21,7 +21,8 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the Cloudflare API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using Cloudflare for DNS).' + description = ('Obtain certificates using a DNS TXT record (if you are using Cloudflare for ' + 'DNS).') ttl = 120 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py index 2e9d23a88..674194fee 100644 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py +++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py @@ -22,7 +22,7 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the CloudXNS DNS API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using CloudXNS for DNS).' + description = 'Obtain certificates using a DNS TXT record (if you are using CloudXNS for DNS).' ttl = 60 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py b/certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py index f489f889a..f3a98567e 100644 --- a/certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py +++ b/certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py @@ -22,7 +22,7 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the DNSimple v2 API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using DNSimple for DNS).' + description = 'Obtain certificates using a DNS TXT record (if you are using DNSimple for DNS).' ttl = 60 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-google/certbot_dns_google/dns_google.py b/certbot-dns-google/certbot_dns_google/dns_google.py index 908c020e1..39811782e 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google.py +++ b/certbot-dns-google/certbot_dns_google/dns_google.py @@ -25,7 +25,8 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the Google Cloud DNS API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using Google Cloud DNS for DNS).' + description = ('Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS ' + 'for DNS).') ttl = 60 def __init__(self, *args, **kwargs): diff --git a/certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py b/certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py index be60ff39d..28db126c1 100644 --- a/certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py +++ b/certbot-dns-nsone/certbot_dns_nsone/dns_nsone.py @@ -22,7 +22,7 @@ class Authenticator(dns_common.DNSAuthenticator): This Authenticator uses the NS1 API to fulfill a dns-01 challenge. """ - description = 'Obtain certs using a DNS TXT record (if you are using NS1 for DNS).' + description = 'Obtain certificates using a DNS TXT record (if you are using NS1 for DNS).' ttl = 60 def __init__(self, *args, **kwargs): diff --git a/certbot-route53/certbot_route53/authenticator.py b/certbot-route53/certbot_route53/authenticator.py index 96b4d1abb..b2a9821e9 100644 --- a/certbot-route53/certbot_route53/authenticator.py +++ b/certbot-route53/certbot_route53/authenticator.py @@ -28,7 +28,7 @@ class Authenticator(common.Plugin): This authenticator solves a DNS01 challenge by uploading the answer to AWS Route53. """ - description = "Obtain certs using a DNS TXT record (if you are using AWS Route53 for DNS)." + description = "Obtain certificates using a DNS TXT record (if you are using AWS Route53 for DNS)." def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) From f0e1be55d693d50794699775c768a3156645babb Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Mon, 5 Jun 2017 21:20:17 -0700 Subject: [PATCH 011/125] route53: make sleep duration configurable like other DNS authenticators (#4771) * Re-structure perform to allow for easier refactoring * Refactor to use dns_common * Make ttl a class variable, like other plugins --- .../certbot_route53/authenticator.py | 60 +++++++------------ .../certbot_route53/authenticator_test.py | 20 +++++-- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/certbot-route53/certbot_route53/authenticator.py b/certbot-route53/certbot_route53/authenticator.py index b2a9821e9..524f6ab36 100644 --- a/certbot-route53/certbot_route53/authenticator.py +++ b/certbot-route53/certbot_route53/authenticator.py @@ -4,17 +4,14 @@ import time import boto3 import zope.interface -from acme import challenges from botocore.exceptions import NoCredentialsError, ClientError from certbot import errors from certbot import interfaces -from certbot.plugins import common +from certbot.plugins import dns_common logger = logging.getLogger(__name__) -TTL = 10 - INSTRUCTIONS = ( "To use certbot-route53, configure credentials as described at " "https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials " # pylint: disable=line-too-long @@ -22,53 +19,41 @@ INSTRUCTIONS = ( @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) -class Authenticator(common.Plugin): +class Authenticator(dns_common.DNSAuthenticator): """Route53 Authenticator This authenticator solves a DNS01 challenge by uploading the answer to AWS Route53. """ - description = "Obtain certificates using a DNS TXT record (if you are using AWS Route53 for DNS)." + + description = ("Obtain certificates using a DNS TXT record (if you are using AWS Route53 for " + "DNS).") + ttl = 10 def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) self.r53 = boto3.client("route53") - def prepare(self): # pylint: disable=missing-docstring,no-self-use - pass # pragma: no cover - def more_info(self): # pylint: disable=missing-docstring,no-self-use return "Solve a DNS01 challenge using AWS Route53" - def get_chall_pref(self, domain): - # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.DNS01] + def _setup_credentials(self): + pass - def perform(self, achalls): # pylint: disable=missing-docstring + def _perform(self, domain, validation_domain_name, validation): try: - change_ids = [ - self._change_txt_record("UPSERT", achall) - for achall in achalls - ] + change_id = self._change_txt_record("UPSERT", validation_domain_name, validation) - for change_id in change_ids: - self._wait_for_change(change_id) - # Sleep for at least the TTL, to ensure that any records cached by - # the ACME server after previous validation attempts are gone. In - # most cases we'll need to wait at least this long for the Route53 - # records to propagate, so this doesn't delay us much. - time.sleep(TTL) - return [achall.response(achall.account_key) for achall in achalls] + self._wait_for_change(change_id) except (NoCredentialsError, ClientError) as e: logger.debug('Encountered error during perform: %s', e, exc_info=True) raise errors.PluginError("\n".join([str(e), INSTRUCTIONS])) - def cleanup(self, achalls): # pylint: disable=missing-docstring - for achall in achalls: - try: - self._change_txt_record("DELETE", achall) - except (NoCredentialsError, ClientError) as e: - logger.debug('Encountered error during cleanup: %s', e, exc_info=True) + def _cleanup(self, domain, validation_domain_name, validation): + try: + self._change_txt_record("DELETE", validation_domain_name, validation) + except (NoCredentialsError, ClientError) as e: + logger.debug('Encountered error during cleanup: %s', e, exc_info=True) def _find_zone_id_for_domain(self, domain): """Find the zone id responsible a given FQDN. @@ -100,11 +85,8 @@ class Authenticator(common.Plugin): zones.sort(key=lambda z: len(z[0]), reverse=True) return zones[0][1] - def _change_txt_record(self, action, achall): - domain = achall.validation_domain_name(achall.domain) - value = achall.validation(achall.account_key) - - zone_id = self._find_zone_id_for_domain(domain) + def _change_txt_record(self, action, validation_domain_name, validation): + zone_id = self._find_zone_id_for_domain(validation_domain_name) response = self.r53.change_resource_record_sets( HostedZoneId=zone_id, @@ -114,13 +96,13 @@ class Authenticator(common.Plugin): { "Action": action, "ResourceRecordSet": { - "Name": domain, + "Name": validation_domain_name, "Type": "TXT", - "TTL": TTL, + "TTL": self.ttl, "ResourceRecords": [ # For some reason TXT records need to be # manually quoted. - {"Value": '"{0}"'.format(value)} + {"Value": '"{0}"'.format(validation)} ], } } diff --git a/certbot-route53/certbot_route53/authenticator_test.py b/certbot-route53/certbot_route53/authenticator_test.py index f16224d84..6e2896e5d 100644 --- a/certbot-route53/certbot_route53/authenticator_test.py +++ b/certbot-route53/certbot_route53/authenticator_test.py @@ -7,6 +7,7 @@ from botocore.exceptions import NoCredentialsError, ClientError from certbot import errors from certbot.plugins import dns_test_common +from certbot.plugins.dns_test_common import DOMAIN class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest): @@ -21,16 +22,15 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest self.auth = Authenticator(self.config, "route53") - def test_parser_arguments(self): - pass # TODO: follow convention of defining optional argument for DNS propagation delay - def test_perform(self): self.auth._change_txt_record = mock.MagicMock() self.auth._wait_for_change = mock.MagicMock() self.auth.perform([self.achall]) - self.auth._change_txt_record.assert_called_once_with("UPSERT", self.achall) + self.auth._change_txt_record.assert_called_once_with("UPSERT", + '_acme-challenge.' + DOMAIN, + mock.ANY) self.auth._wait_for_change.assert_called_once() def test_perform_no_credentials_error(self): @@ -49,18 +49,26 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest [self.achall]) def test_cleanup(self): + self.auth._attempt_cleanup = True + self.auth._change_txt_record = mock.MagicMock() self.auth.cleanup([self.achall]) - self.auth._change_txt_record.assert_called_once_with("DELETE", self.achall) + self.auth._change_txt_record.assert_called_once_with("DELETE", + '_acme-challenge.'+DOMAIN, + mock.ANY) def test_cleanup_no_credentials_error(self): + self.auth._attempt_cleanup = True + self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) self.auth.cleanup([self.achall]) def test_cleanup_client_error(self): + self.auth._attempt_cleanup = True + self.auth._change_txt_record = mock.MagicMock( side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) @@ -173,7 +181,7 @@ class ClientTest(unittest.TestCase): self.client.r53.change_resource_record_sets = mock.MagicMock( return_value={"ChangeInfo": {"Id": 1}}) - self.client._change_txt_record("FOO", dns_test_common.BaseAuthenticatorTest.achall) + self.client._change_txt_record("FOO", DOMAIN, "foo") self.client.r53.change_resource_record_sets.assert_called_once() From 7531c9891633bf777abc0230241d45399f0cbd7a Mon Sep 17 00:00:00 2001 From: Schuyler Duveen Date: Tue, 6 Jun 2017 13:11:33 -0400 Subject: [PATCH 012/125] =?UTF-8?q?fixes=20#3616:=20make=20sure=20there=20?= =?UTF-8?q?is=20always=20one=20time=20that=20we=20test=20the=20sock?= =?UTF-8?q?=E2=80=A6=20(#4712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- acme/acme/standalone_test.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 613258c97..c3beab34b 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -137,7 +137,6 @@ class TestSimpleTLSSNI01Server(unittest.TestCase): ) self.old_cwd = os.getcwd() os.chdir(self.test_cwd) - self.thread.start() def tearDown(self): os.chdir(self.old_cwd) @@ -146,13 +145,12 @@ class TestSimpleTLSSNI01Server(unittest.TestCase): def test_it(self): max_attempts = 5 - while max_attempts: - max_attempts -= 1 + for attempt in range(max_attempts): try: cert = crypto_util.probe_sni( b'localhost', b'0.0.0.0', self.port) except errors.Error: - self.assertTrue(max_attempts > 0, "Timeout!") + self.assertTrue(attempt + 1 < max_attempts, "Timeout!") time.sleep(1) # wait until thread starts else: self.assertEqual(jose.ComparableX509(cert), @@ -160,6 +158,11 @@ class TestSimpleTLSSNI01Server(unittest.TestCase): 'rsa2048_cert.pem')) break + if attempt == 0: + # the first attempt is always meant to fail, so we can test + # the socket failure code-path for probe_sni, as well + self.thread.start() + if __name__ == "__main__": unittest.main() # pragma: no cover From e7499374655e5e3ac1da2e3bec283710354a92be Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Tue, 6 Jun 2017 15:41:04 -0700 Subject: [PATCH 013/125] route53: rename to match other DNS plugins (#4747) This change renames certbot-route53 to certbot-dns-route53 and updates the package's setup.py file to maintain backwards compatibility. Testing Done: * Run `certbot` with `-a certbot-route53:auth`, verify the plugin runs. * Run `certbot` with `--dns-route53`, verify the plugin runs. --- {certbot-route53 => certbot-dns-route53}/.gitignore | 0 {certbot-route53 => certbot-dns-route53}/LICENSE | 0 {certbot-route53 => certbot-dns-route53}/MANIFEST.in | 0 {certbot-route53 => certbot-dns-route53}/README.md | 2 +- .../certbot_dns_route53}/__init__.py | 0 .../certbot_dns_route53/authenticator.py | 12 ++++++++++++ .../certbot_dns_route53/dns_route53.py | 4 ++-- .../certbot_dns_route53/dns_route53_test.py | 6 +++--- .../examples/sample-aws-policy.json | 2 +- {certbot-route53 => certbot-dns-route53}/setup.cfg | 0 {certbot-route53 => certbot-dns-route53}/setup.py | 7 ++++--- .../tools/tester.pkoch-macos_sierra.sh | 0 certbot/cli.py | 3 +++ certbot/plugins/disco.py | 1 + certbot/plugins/selection.py | 5 ++++- tools/venv.sh | 2 +- tools/venv3.sh | 2 +- tox.cover.sh | 6 +++--- tox.ini | 8 ++++---- 19 files changed, 40 insertions(+), 20 deletions(-) rename {certbot-route53 => certbot-dns-route53}/.gitignore (100%) rename {certbot-route53 => certbot-dns-route53}/LICENSE (100%) rename {certbot-route53 => certbot-dns-route53}/MANIFEST.in (100%) rename {certbot-route53 => certbot-dns-route53}/README.md (97%) rename {certbot-route53/certbot_route53 => certbot-dns-route53/certbot_dns_route53}/__init__.py (100%) create mode 100644 certbot-dns-route53/certbot_dns_route53/authenticator.py rename certbot-route53/certbot_route53/authenticator.py => certbot-dns-route53/certbot_dns_route53/dns_route53.py (96%) rename certbot-route53/certbot_route53/authenticator_test.py => certbot-dns-route53/certbot_dns_route53/dns_route53_test.py (97%) rename {certbot-route53 => certbot-dns-route53}/examples/sample-aws-policy.json (91%) rename {certbot-route53 => certbot-dns-route53}/setup.cfg (100%) rename {certbot-route53 => certbot-dns-route53}/setup.py (88%) rename {certbot-route53 => certbot-dns-route53}/tools/tester.pkoch-macos_sierra.sh (100%) diff --git a/certbot-route53/.gitignore b/certbot-dns-route53/.gitignore similarity index 100% rename from certbot-route53/.gitignore rename to certbot-dns-route53/.gitignore diff --git a/certbot-route53/LICENSE b/certbot-dns-route53/LICENSE similarity index 100% rename from certbot-route53/LICENSE rename to certbot-dns-route53/LICENSE diff --git a/certbot-route53/MANIFEST.in b/certbot-dns-route53/MANIFEST.in similarity index 100% rename from certbot-route53/MANIFEST.in rename to certbot-dns-route53/MANIFEST.in diff --git a/certbot-route53/README.md b/certbot-dns-route53/README.md similarity index 97% rename from certbot-route53/README.md rename to certbot-dns-route53/README.md index 582a0fb35..4af66aa00 100644 --- a/certbot-route53/README.md +++ b/certbot-dns-route53/README.md @@ -30,6 +30,6 @@ To generate a certificate: ``` certbot certonly \ -n --agree-tos --email DEVOPS@COMPANY.COM \ - -a certbot-route53:auth \ + --dns-route53 \ -d MY.DOMAIN.NAME ``` diff --git a/certbot-route53/certbot_route53/__init__.py b/certbot-dns-route53/certbot_dns_route53/__init__.py similarity index 100% rename from certbot-route53/certbot_route53/__init__.py rename to certbot-dns-route53/certbot_dns_route53/__init__.py diff --git a/certbot-dns-route53/certbot_dns_route53/authenticator.py b/certbot-dns-route53/certbot_dns_route53/authenticator.py new file mode 100644 index 000000000..0c612e57c --- /dev/null +++ b/certbot-dns-route53/certbot_dns_route53/authenticator.py @@ -0,0 +1,12 @@ +"""Shim around `~certbot_dns_route53.dns_route53` for backwards compatibility.""" +import warnings + +from certbot_dns_route53 import dns_route53 + + +class Authenticator(dns_route53.Authenticator): + """Shim around `~certbot_dns_route53.dns_route53.Authenticator` for backwards compatibility.""" + def __init__(self, *args, **kwargs): + warnings.warn("The 'authenticator' module was renamed 'dns_route53'", + DeprecationWarning) + super(Authenticator, self).__init__(*args, **kwargs) diff --git a/certbot-route53/certbot_route53/authenticator.py b/certbot-dns-route53/certbot_dns_route53/dns_route53.py similarity index 96% rename from certbot-route53/certbot_route53/authenticator.py rename to certbot-dns-route53/certbot_dns_route53/dns_route53.py index 524f6ab36..67462e369 100644 --- a/certbot-route53/certbot_route53/authenticator.py +++ b/certbot-dns-route53/certbot_dns_route53/dns_route53.py @@ -13,7 +13,7 @@ from certbot.plugins import dns_common logger = logging.getLogger(__name__) INSTRUCTIONS = ( - "To use certbot-route53, configure credentials as described at " + "To use certbot-dns-route53, configure credentials as described at " "https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials " # pylint: disable=line-too-long "and add the necessary permissions for Route53 access.") @@ -91,7 +91,7 @@ class Authenticator(dns_common.DNSAuthenticator): response = self.r53.change_resource_record_sets( HostedZoneId=zone_id, ChangeBatch={ - "Comment": "certbot-route53 certificate validation " + action, + "Comment": "certbot-dns-route53 certificate validation " + action, "Changes": [ { "Action": action, diff --git a/certbot-route53/certbot_route53/authenticator_test.py b/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py similarity index 97% rename from certbot-route53/certbot_route53/authenticator_test.py rename to certbot-dns-route53/certbot_dns_route53/dns_route53_test.py index 6e2896e5d..ff07b6ccd 100644 --- a/certbot-route53/certbot_route53/authenticator_test.py +++ b/certbot-dns-route53/certbot_dns_route53/dns_route53_test.py @@ -1,4 +1,4 @@ -"""Tests for certbot_route53.authenticator""" +"""Tests for certbot_dns_route53.dns_route53.Authenticator""" import unittest @@ -14,7 +14,7 @@ class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest # pylint: disable=protected-access def setUp(self): - from certbot_route53.authenticator import Authenticator + from certbot_dns_route53.dns_route53 import Authenticator super(AuthenticatorTest, self).setUp() @@ -111,7 +111,7 @@ class ClientTest(unittest.TestCase): } def setUp(self): - from certbot_route53.authenticator import Authenticator + from certbot_dns_route53.dns_route53 import Authenticator super(ClientTest, self).setUp() diff --git a/certbot-route53/examples/sample-aws-policy.json b/certbot-dns-route53/examples/sample-aws-policy.json similarity index 91% rename from certbot-route53/examples/sample-aws-policy.json rename to certbot-dns-route53/examples/sample-aws-policy.json index 0b4dcae41..10a17de19 100644 --- a/certbot-route53/examples/sample-aws-policy.json +++ b/certbot-dns-route53/examples/sample-aws-policy.json @@ -1,6 +1,6 @@ { "Version": "2012-10-17", - "Id": "certbot-route53 sample policy", + "Id": "certbot-dns-route53 sample policy", "Statement": [ { "Effect": "Allow", diff --git a/certbot-route53/setup.cfg b/certbot-dns-route53/setup.cfg similarity index 100% rename from certbot-route53/setup.cfg rename to certbot-dns-route53/setup.cfg diff --git a/certbot-route53/setup.py b/certbot-dns-route53/setup.py similarity index 88% rename from certbot-route53/setup.py rename to certbot-dns-route53/setup.py index 40a104a40..8d2632697 100644 --- a/certbot-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -17,7 +17,7 @@ install_requires = [ ] setup( - name='certbot-route53', + name='certbot-dns-route53', version=version, description="Route53 DNS Authenticator plugin for Certbot", url='https://github.com/certbot/certbot', @@ -52,8 +52,9 @@ setup( keywords=['certbot', 'route53', 'aws'], entry_points={ 'certbot.plugins': [ - 'auth = certbot_route53.authenticator:Authenticator' + 'dns-route53 = certbot_dns_route53.dns_route53:Authenticator', + 'certbot-route53:auth = certbot_dns_route53.dns_route53:Authenticator' ], }, - test_suite='certbot_route53', + test_suite='certbot_dns_route53', ) diff --git a/certbot-route53/tools/tester.pkoch-macos_sierra.sh b/certbot-dns-route53/tools/tester.pkoch-macos_sierra.sh similarity index 100% rename from certbot-route53/tools/tester.pkoch-macos_sierra.sh rename to certbot-dns-route53/tools/tester.pkoch-macos_sierra.sh diff --git a/certbot/cli.py b/certbot/cli.py index 78e4032f1..a74b50636 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1249,6 +1249,9 @@ def _plugins_parsing(helpful, plugins): helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are ' 'using NS1 for DNS).')) + helpful.add(["plugins", "certonly"], "--dns-route53", action="store_true", + help=('Obtain certificates using a DNS TXT record (if you are using Route53 for ' + 'DNS).')) # things should not be reorder past/pre this comment: # plugins_group should be displayed in --help before plugin diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index af577564c..9a229d618 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -34,6 +34,7 @@ class PluginEntryPoint(object): "certbot-dns-dnsimple", "certbot-dns-google", "certbot-dns-nsone", + "certbot-dns-route53", "certbot-nginx", ] """Distributions for which prefix will be omitted.""" diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index eb41d5b93..15008302d 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -134,7 +134,8 @@ def choose_plugin(prepared, question): return None noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", - "dns-digitalocean", "dns-dnsimple", "dns-google", "dns-nsone"] + "dns-digitalocean", "dns-dnsimple", "dns-google", "dns-route53", + "dns-nsone"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -250,6 +251,8 @@ def cli_plugin_requests(config): req_auth = set_configurator(req_auth, "dns-google") if config.dns_nsone: req_auth = set_configurator(req_auth, "dns-nsone") + if config.dns_route53: + req_auth = set_configurator(req_auth, "dns-route53") logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) return req_auth, req_inst diff --git a/tools/venv.sh b/tools/venv.sh index 75ef017be..2d8e4f242 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -20,7 +20,7 @@ fi -e certbot-dns-dnsimple \ -e certbot-dns-google \ -e certbot-dns-nsone \ + -e certbot-dns-route53 \ -e certbot-nginx \ - -e certbot-route53 \ -e letshelp-certbot \ -e certbot-compatibility-test diff --git a/tools/venv3.sh b/tools/venv3.sh index 6f5d96262..943507637 100755 --- a/tools/venv3.sh +++ b/tools/venv3.sh @@ -19,7 +19,7 @@ fi -e certbot-dns-dnsimple \ -e certbot-dns-google \ -e certbot-dns-nsone \ + -e certbot-dns-route53 \ -e certbot-nginx \ - -e certbot-route53 \ -e letshelp-certbot \ -e certbot-compatibility-test diff --git a/tox.cover.sh b/tox.cover.sh index eabe6ba7e..db8a6c500 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_google certbot_dns_nsone certbot_nginx certbot_route53 letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_google certbot_dns_nsone certbot_dns_route53 certbot_nginx letshelp_certbot" else pkgs="$@" fi @@ -33,10 +33,10 @@ cover () { min=99 elif [ "$1" = "certbot_dns_nsone" ]; then min=99 + elif [ "$1" = "certbot_dns_route53" ]; then + min=99 elif [ "$1" = "certbot_nginx" ]; then min=97 - elif [ "$1" = "certbot_route53" ]; then - min=99 elif [ "$1" = "letshelp_certbot" ]; then min=100 else diff --git a/tox.ini b/tox.ini index 414dac790..8279a687f 100644 --- a/tox.ini +++ b/tox.ini @@ -37,10 +37,10 @@ dns_plugin_commands = nosetests -v certbot_dns_digitalocean --processes=-1 pip install -e certbot-dns-google nosetests -v certbot_dns_google --processes=-1 - pip install -e certbot-route53 - nosetests -v certbot_route53 --processes=-1 --process-timeout=25 -dns_plugin_install_args = -e certbot-dns-cloudflare -e certbot-dns-digitalocean -e certbot-dns-google -e certbot-route53 -dns_plugin_paths = certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-google/certbot_dns_google certbot-route53/certbot_route53 + pip install -e certbot-dns-route53 + nosetests -v certbot_dns_route53 --processes=-1 --process-timeout=25 +dns_plugin_install_args = -e certbot-dns-cloudflare -e certbot-dns-digitalocean -e certbot-dns-google -e certbot-dns-route53 +dns_plugin_paths = certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-google/certbot_dns_google certbot-dns-route53/certbot_dns_route53 lexicon_dns_plugin_commands = pip install -e certbot-dns-cloudxns From e0f3c05c02fdf39e1801b2f56d6b52eef7e2ed54 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 6 Jun 2017 15:48:00 -0700 Subject: [PATCH 014/125] Fix test_apache2.sh test farm test. (#4786) tools/venv.sh cannot be used as the tests run on systems with Python 2.6 and tools/venv.sh installs code that is not compatible with Python 2.6. --- tests/letstest/scripts/test_apache2.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/letstest/scripts/test_apache2.sh b/tests/letstest/scripts/test_apache2.sh index 7fa860302..6b5d63c80 100755 --- a/tests/letstest/scripts/test_apache2.sh +++ b/tests/letstest/scripts/test_apache2.sh @@ -45,7 +45,7 @@ if [ $? -ne 0 ] ; then exit 1 fi -tools/venv.sh +tools/_venv_common.sh -e acme[dev] -e .[dev,docs] -e certbot-apache sudo venv/bin/certbot -v --debug --text --agree-dev-preview --agree-tos \ --renew-by-default --redirect --register-unsafely-without-email \ --domain $PUBLIC_HOSTNAME --server $BOULDER_URL From af8dae6cb2f3ea826077c2eda051bc9c609e87ec Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Tue, 6 Jun 2017 15:51:16 -0700 Subject: [PATCH 015/125] Check domains for accidental inclusion of a scheme (#4788) Currently, accidentally including a scheme with the domain name does not produce a particularly helpful error message. Examples without this change: 1. `certbot certonly -d https://test.example.com --webroot`: Saving debug log to /tmp/certbot/logs/letsencrypt.log Obtaining a new certificate An unexpected error occurred: The request message was malformed :: Error creating new authz :: Invalid character in DNS name Please see the logfiles in /tmp/certbot/logs for more details. 2. `certbot certonly -d http://hoeveelmensengaveneeneuroomtezienhoeveelmenseneeneurogaven.example.com` Requested domain http://hoeveelmensengaveneeneuroomtezienhoeveelmenseneeneurogaven.example.com is not a FQDN because label http://hoeveelmensengaveneeneuroomtezienhoeveelmenseneeneurogaven is too long. Examples with this change: 1. `certbot certonly -d https://test.example.com --webroot`: Requested name https://test.example.com appears to be a URL, not a FQDN. Try again without the leading "https://". 2. `certbot certonly -d http://hoeveelmensengaveneeneuroomtezienhoeveelmenseneeneurogaven.example.com` Requested name http://hoeveelmensengaveneeneuroomtezienhoeveelmenseneeneurogaven.example.com appears to be a URL, not a FQDN. Try again without the leading "http://". (Resolves #4785) --- certbot/tests/util_test.py | 7 +++++++ certbot/util.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 3f6bd2a39..7e320012a 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -420,6 +420,13 @@ class EnforceLeValidity(unittest.TestCase): def test_valid_domain(self): self.assertEqual(self._call(u"example.com"), u"example.com") + def test_input_with_scheme(self): + self.assertRaises(errors.ConfigurationError, self._call, u"http://example.com") + self.assertRaises(errors.ConfigurationError, self._call, u"https://example.com") + + def test_valid_input_with_scheme_name(self): + self.assertEqual(self._call(u"http.example.com"), u"http.example.com") + class EnforceDomainSanityTest(unittest.TestCase): """Test enforce_domain_sanity.""" diff --git a/certbot/util.py b/certbot/util.py index 041515199..a95ef62b9 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -568,6 +568,17 @@ def enforce_domain_sanity(domain): # Remove trailing dot domain = domain[:-1] if domain.endswith(u'.') else domain + # Separately check for odd "domains" like "http://example.com" to fail + # fast and provide a clear error message + for scheme in ["http", "https"]: # Other schemes seem unlikely + if domain.startswith("{0}://".format(scheme)): + raise errors.ConfigurationError( + "Requested name {0} appears to be a URL, not a FQDN. " + "Try again without the leading \"{1}://\".".format( + domain, scheme + ) + ) + # Explain separately that IP addresses aren't allowed (apart from not # being FQDNs) because hope springs eternal concerning this point try: From 239184882e1694bee62b12e43e3d4e0a1f08b9da Mon Sep 17 00:00:00 2001 From: ohemorange Date: Tue, 6 Jun 2017 17:04:45 -0700 Subject: [PATCH 016/125] Enable IPv6 support in standalone plugin (#4773) * add TLSSNI01DualNetworkedServers * use DualNetworkedServers in certbot/plugins/standalone.py also, make both servers run on the same port. * make probe_sni connect on ipv6 and ipv4 using None * mimic BSD-like conditions to get test coverage * test ServerManager taking into account BSD systems * pass tests even if python is compiled without ipv6 support --- acme/acme/crypto_util.py | 9 +- acme/acme/standalone.py | 108 ++++++++++++++++++++-- acme/acme/standalone_test.py | 139 +++++++++++++++++++++++++++++ certbot/plugins/standalone.py | 59 ++++++------ certbot/plugins/standalone_test.py | 11 ++- 5 files changed, 282 insertions(+), 44 deletions(-) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index f86a9971a..84b70e4a6 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -107,7 +107,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods def probe_sni(name, host, port=443, timeout=300, - method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)): + method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('', 0)): """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the @@ -132,9 +132,14 @@ def probe_sni(name, host, port=443, timeout=300, socket_kwargs = {} if sys.version_info < (2, 7) else { 'source_address': source_address} + host_protocol_agnostic = None if host == '::' or host == '0' else host + try: # pylint: disable=star-args - sock = socket.create_connection((host, port), **socket_kwargs) + logger.debug("Attempting to connect to %s:%d%s.", host_protocol_agnostic, port, + " from {0}:{1}".format(source_address[0], source_address[1]) if \ + socket_kwargs else "") + sock = socket.create_connection((host_protocol_agnostic, port), **socket_kwargs) except socket.error as error: raise errors.Error(error) diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 087240c15..c221f5883 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -4,7 +4,9 @@ import collections import functools import logging import os +import socket import sys +import threading from six.moves import BaseHTTPServer # type: ignore # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error @@ -26,6 +28,11 @@ class TLSServer(socketserver.TCPServer): """Generic TLS Server.""" def __init__(self, *args, **kwargs): + self.ipv6 = kwargs.pop("ipv6", False) + if self.ipv6: + self.address_family = socket.AF_INET6 + else: + self.address_family = socket.AF_INET self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop( # pylint: disable=protected-access @@ -49,12 +56,81 @@ class ACMEServerMixin: # pylint: disable=old-style-class allow_reuse_address = True +class BaseDualNetworkedServers(object): + """Base class for a pair of IPv6 and IPv4 servers that tries to do everything + it's asked for both servers, but where failures in one server don't + affect the other. + + If two servers are instantiated, they will serve on the same port. + """ + + def __init__(self, ServerClass, server_address, *remaining_args, **kwargs): + port = server_address[1] + self.threads = [] + self.servers = [] + + # Must try True first. + # Ubuntu, for example, will fail to bind to IPv4 if we've already bound + # to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6 + # socket. On the other hand, FreeBSD will successfully bind to IPv4 on the + # same port, which means that server will accept the IPv4 connections. + # If Python is compiled without IPv6, we'll error out but (probably) successfully + # create the IPv4 server. + for ip_version in [True, False]: + try: + kwargs["ipv6"] = ip_version + new_address = (server_address[0],) + (port,) + server_address[2:] + new_args = (new_address,) + remaining_args + server = ServerClass(*new_args, **kwargs) # pylint: disable=star-args + except socket.error: + logger.debug("Failed to bind to %s:%s using %s", new_address[0], + new_address[1], "IPv6" if ip_version else "IPv4") + else: + self.servers.append(server) + # If two servers are set up and port 0 was passed in, ensure we always + # bind to the same port for both servers. + port = server.socket.getsockname()[1] + if len(self.servers) == 0: + raise socket.error("Could not bind to IPv4 or IPv6.") + + def serve_forever(self): + """Wraps socketserver.TCPServer.serve_forever""" + for server in self.servers: + thread = threading.Thread( + # pylint: disable=no-member + target=server.serve_forever) + thread.start() + self.threads.append(thread) + + def getsocknames(self): + """Wraps socketserver.TCPServer.socket.getsockname""" + return [server.socket.getsockname() for server in self.servers] + + def shutdown_and_server_close(self): + """Wraps socketserver.TCPServer.shutdown, socketserver.TCPServer.server_close, and + threading.Thread.join""" + for server in self.servers: + server.shutdown() + server.server_close() + for thread in self.threads: + thread.join() + self.threads = [] + + class TLSSNI01Server(TLSServer, ACMEServerMixin): """TLSSNI01 Server.""" - def __init__(self, server_address, certs): + def __init__(self, server_address, certs, ipv6=False): TLSServer.__init__( - self, server_address, BaseRequestHandlerWithLogging, certs=certs) + self, server_address, BaseRequestHandlerWithLogging, certs=certs, ipv6=ipv6) + + +class TLSSNI01DualNetworkedServers(BaseDualNetworkedServers): + """TLSSNI01Server Wrapper. Tries everything for both. Failures for one don't + affect the other.""" + + def __init__(self, *args, **kwargs): + BaseDualNetworkedServers.__init__(self, TLSSNI01Server, *args, **kwargs) class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): @@ -70,13 +146,33 @@ class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): socketserver.BaseRequestHandler.handle(self) -class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin): +class HTTPServer(BaseHTTPServer.HTTPServer): + """Generic HTTP Server.""" + + def __init__(self, *args, **kwargs): + self.ipv6 = kwargs.pop("ipv6", False) + if self.ipv6: + self.address_family = socket.AF_INET6 + else: + self.address_family = socket.AF_INET + BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) + + +class HTTP01Server(HTTPServer, ACMEServerMixin): """HTTP01 Server.""" - def __init__(self, server_address, resources): - BaseHTTPServer.HTTPServer.__init__( + def __init__(self, server_address, resources, ipv6=False): + HTTPServer.__init__( self, server_address, HTTP01RequestHandler.partial_init( - simple_http_resources=resources)) + simple_http_resources=resources), ipv6=ipv6) + + +class HTTP01DualNetworkedServers(BaseDualNetworkedServers): + """HTTP01Server Wrapper. Tries everything for both. Failures for one don't + affect the other.""" + + def __init__(self, *args, **kwargs): + BaseDualNetworkedServers.__init__(self, HTTP01Server, *args, **kwargs) class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index c3beab34b..16669680c 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -1,6 +1,7 @@ """Tests for acme.standalone.""" import os import shutil +import socket import threading import tempfile import time @@ -9,6 +10,7 @@ import unittest from six.moves import http_client # pylint: disable=import-error from six.moves import socketserver # type: ignore # pylint: disable=import-error +import mock import requests from acme import challenges @@ -29,6 +31,13 @@ class TLSServerTest(unittest.TestCase): ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True) server.server_close() # pylint: disable=no-member + def test_ipv6(self): + if socket.has_ipv6: + from acme.standalone import TLSServer + server = TLSServer( + ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True, ipv6=True) + server.server_close() # pylint: disable=no-member + class TLSSNI01ServerTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01Server.""" @@ -112,6 +121,136 @@ class HTTP01ServerTest(unittest.TestCase): self.assertFalse(self._test_http01(add=False)) +class BaseDualNetworkedServersTest(unittest.TestCase): + """Test for acme.standalone.BaseDualNetworkedServers.""" + + _multiprocess_can_split_ = True + + class SingleProtocolServer(socketserver.TCPServer): + """Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6.""" + def __init__(self, *args, **kwargs): + ipv6 = kwargs.pop("ipv6", False) + if ipv6: + self.address_family = socket.AF_INET6 + kwargs["bind_and_activate"] = False + else: + self.address_family = socket.AF_INET + socketserver.TCPServer.__init__(self, *args, **kwargs) + if ipv6: + # pylint: disable=no-member + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + try: + self.server_bind() + self.server_activate() + except: + self.server_close() + raise + + @mock.patch("socket.socket.bind") + def test_fail_to_bind(self, mock_bind): + mock_bind.side_effect = socket.error + from acme.standalone import BaseDualNetworkedServers + self.assertRaises(socket.error, BaseDualNetworkedServers, + BaseDualNetworkedServersTest.SingleProtocolServer, + ("", 0), + socketserver.BaseRequestHandler) + + def test_ports_equal(self): + from acme.standalone import BaseDualNetworkedServers + servers = BaseDualNetworkedServers( + BaseDualNetworkedServersTest.SingleProtocolServer, + ("", 0), + socketserver.BaseRequestHandler) + socknames = servers.getsocknames() + prev_port = None + # assert ports are equal + for sockname in socknames: + port = sockname[1] + if prev_port: + self.assertEqual(prev_port, port) + prev_port = port + + +class TLSSNI01DualNetworkedServersTest(unittest.TestCase): + """Test for acme.standalone.TLSSNI01DualNetworkedServers.""" + + _multiprocess_can_split_ = True + + def setUp(self): + self.certs = {b'localhost': ( + test_util.load_pyopenssl_private_key('rsa2048_key.pem'), + test_util.load_cert('rsa2048_cert.pem'), + )} + from acme.standalone import TLSSNI01DualNetworkedServers + self.servers = TLSSNI01DualNetworkedServers(("", 0), certs=self.certs) + self.servers.serve_forever() + + def tearDown(self): + self.servers.shutdown_and_server_close() + + def test_connect(self): + socknames = self.servers.getsocknames() + # connect to all addresses + for sockname in socknames: + host, port = sockname[:2] + cert = crypto_util.probe_sni( + b'localhost', host=host, port=port, timeout=1) + self.assertEqual(jose.ComparableX509(cert), + jose.ComparableX509(self.certs[b'localhost'][1])) + + +class HTTP01DualNetworkedServersTest(unittest.TestCase): + """Tests for acme.standalone.HTTP01DualNetworkedServers.""" + + _multiprocess_can_split_ = True + + def setUp(self): + self.account_key = jose.JWK.load( + test_util.load_vector('rsa1024_key.pem')) + self.resources = set() + + from acme.standalone import HTTP01DualNetworkedServers + self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources) + + # pylint: disable=no-member + self.port = self.servers.getsocknames()[0][1] + self.servers.serve_forever() + + def tearDown(self): + self.servers.shutdown_and_server_close() + + def test_index(self): + response = requests.get( + 'http://localhost:{0}'.format(self.port), verify=False) + self.assertEqual( + response.text, 'ACME client standalone challenge solver') + self.assertTrue(response.ok) + + def test_404(self): + response = requests.get( + 'http://localhost:{0}/foo'.format(self.port), verify=False) + self.assertEqual(response.status_code, http_client.NOT_FOUND) + + def _test_http01(self, add): + chall = challenges.HTTP01(token=(b'x' * 16)) + response, validation = chall.response_and_validation(self.account_key) + + from acme.standalone import HTTP01RequestHandler + resource = HTTP01RequestHandler.HTTP01Resource( + chall=chall, response=response, validation=validation) + if add: + self.resources.add(resource) + return resource.response.simple_verify( + resource.chall, 'localhost', self.account_key.public_key(), + port=self.port) + + def test_http01_found(self): + self.assertTrue(self._test_http01(add=True)) + + def test_http01_not_found(self): + self.assertFalse(self._test_http01(add=False)) + + class TestSimpleTLSSNI01Server(unittest.TestCase): """Tests for acme.standalone.simple_tls_sni_01_server.""" diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index ce878f84a..817403bd3 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -3,7 +3,6 @@ import argparse import collections import logging import socket -import threading import OpenSSL import six @@ -33,8 +32,6 @@ class ServerManager(object): will serve the same URLs! """ - _Instance = collections.namedtuple("_Instance", "server thread") - def __init__(self, certs, http_01_resources): self._instances = {} self.certs = certs @@ -51,34 +48,32 @@ class ServerManager(object): either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`. :param str listenaddr: (optional) The address to listen on. Defaults to all addrs. - :returns: Server instance. + :returns: DualNetworkedServers instance. :rtype: ACMEServerMixin """ assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01) if port in self._instances: - return self._instances[port].server + return self._instances[port] address = (listenaddr, port) try: if challenge_type is challenges.TLSSNI01: - server = acme_standalone.TLSSNI01Server(address, self.certs) + servers = acme_standalone.TLSSNI01DualNetworkedServers(address, self.certs) else: # challenges.HTTP01 - server = acme_standalone.HTTP01Server( + servers = acme_standalone.HTTP01DualNetworkedServers( address, self.http_01_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) - thread = threading.Thread( - # pylint: disable=no-member - target=server.serve_forever) - thread.start() + servers.serve_forever() # if port == 0, then random free port on OS is taken # pylint: disable=no-member - real_port = server.socket.getsockname()[1] - self._instances[real_port] = self._Instance(server, thread) - return server + # both servers, if they exist, have the same port + real_port = servers.getsocknames()[0][1] + self._instances[real_port] = servers + return servers def stop(self, port): """Stop ACME server running on the specified ``port``. @@ -87,13 +82,12 @@ class ServerManager(object): """ instance = self._instances[port] - logger.debug("Stopping server at %s:%d...", - *instance.server.socket.getsockname()[:2]) - instance.server.shutdown() + for sockname in instance.getsocknames(): + logger.debug("Stopping server at %s:%d...", + *sockname[:2]) # Not calling server_close causes problems when renewing multiple # certs with `certbot renew` using TLSSNI01 and PyOpenSSL 0.13 - instance.server.server_close() - instance.thread.join() + instance.shutdown_and_server_close() del self._instances[port] def running(self): @@ -102,12 +96,11 @@ class ServerManager(object): Once the server is stopped using `stop`, it will not be returned. - :returns: Mapping from ``port`` to ``server``. + :returns: Mapping from ``port`` to ``servers``. :rtype: tuple """ - return dict((port, instance.server) for port, instance - in six.iteritems(self._instances)) + return self._instances.copy() SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01] @@ -236,38 +229,38 @@ class Authenticator(common.Plugin): def _perform_single(self, achall): if isinstance(achall.chall, challenges.HTTP01): - server, response = self._perform_http_01(achall) + servers, response = self._perform_http_01(achall) else: # tls-sni-01 - server, response = self._perform_tls_sni_01(achall) - self.served[server].add(achall) + servers, response = self._perform_tls_sni_01(achall) + self.served[servers].add(achall) return response def _perform_http_01(self, achall): port = self.config.http01_port addr = self.config.http01_address - server = self.servers.run(port, challenges.HTTP01, listenaddr=addr) + servers = self.servers.run(port, challenges.HTTP01, listenaddr=addr) response, validation = achall.response_and_validation() resource = acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation) self.http_01_resources.add(resource) - return server, response + return servers, response def _perform_tls_sni_01(self, achall): port = self.config.tls_sni_01_port addr = self.config.tls_sni_01_address - server = self.servers.run(port, challenges.TLSSNI01, listenaddr=addr) + servers = self.servers.run(port, challenges.TLSSNI01, listenaddr=addr) response, (cert, _) = achall.response_and_validation(cert_key=self.key) self.certs[response.z_domain] = (self.key, cert) - return server, response + return servers, response def cleanup(self, achalls): # pylint: disable=missing-docstring - # reduce self.served and close servers if none challenges are served - for server, server_achalls in self.served.items(): + # reduce self.served and close servers if no challenges are served + for unused_servers, server_achalls in self.served.items(): for achall in achalls: if achall in server_achalls: server_achalls.remove(achall) - for port, server in six.iteritems(self.servers.running()): - if not self.served[server]: + for port, servers in six.iteritems(self.servers.running()): + if not self.served[servers]: self.servers.stop(port) diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 65d16c2f2..2a55c516f 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -32,7 +32,7 @@ class ServerManagerTest(unittest.TestCase): def _test_run_stop(self, challenge_type): server = self.mgr.run(port=0, challenge_type=challenge_type) - port = server.socket.getsockname()[1] # pylint: disable=no-member + port = server.getsocknames()[0][1] # pylint: disable=no-member self.assertEqual(self.mgr.running(), {port: server}) self.mgr.stop(port=port) self.assertEqual(self.mgr.running(), {}) @@ -45,7 +45,7 @@ class ServerManagerTest(unittest.TestCase): def test_run_idempotent(self): server = self.mgr.run(port=0, challenge_type=challenges.HTTP01) - port = server.socket.getsockname()[1] # pylint: disable=no-member + port = server.getsocknames()[0][1] # pylint: disable=no-member server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {port: server}) self.assertTrue(server is server2) @@ -53,9 +53,14 @@ class ServerManagerTest(unittest.TestCase): self.assertEqual(self.mgr.running(), {}) def test_run_bind_error(self): - some_server = socket.socket() + some_server = socket.socket(socket.AF_INET6) some_server.bind(("", 0)) port = some_server.getsockname()[1] + maybe_another_server = socket.socket() + try: + maybe_another_server.bind(("", port)) + except socket.error: + pass self.assertRaises( errors.StandaloneBindError, self.mgr.run, port, challenge_type=challenges.HTTP01) From a06dec366023d14277fa4309d0e75c51c36f49ce Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Wed, 7 Jun 2017 15:16:52 -0700 Subject: [PATCH 017/125] route53: avoid listing the plugin twice in the UI (#4794) Without this change, the Route53 plugin is listed twice when running Certbot interactively (once under the old name, once under the new name). This change ensures only the new name is shown, while maintaining hidden backwards compatibility with the old name. --- certbot-dns-route53/certbot_dns_route53/authenticator.py | 8 ++++++++ certbot-dns-route53/setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/certbot-dns-route53/certbot_dns_route53/authenticator.py b/certbot-dns-route53/certbot_dns_route53/authenticator.py index 0c612e57c..53215ea1d 100644 --- a/certbot-dns-route53/certbot_dns_route53/authenticator.py +++ b/certbot-dns-route53/certbot_dns_route53/authenticator.py @@ -1,11 +1,19 @@ """Shim around `~certbot_dns_route53.dns_route53` for backwards compatibility.""" import warnings +import zope.interface + +from certbot import interfaces from certbot_dns_route53 import dns_route53 +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) class Authenticator(dns_route53.Authenticator): """Shim around `~certbot_dns_route53.dns_route53.Authenticator` for backwards compatibility.""" + + hidden = True + def __init__(self, *args, **kwargs): warnings.warn("The 'authenticator' module was renamed 'dns_route53'", DeprecationWarning) diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 8d2632697..a138cdea5 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -53,7 +53,7 @@ setup( entry_points={ 'certbot.plugins': [ 'dns-route53 = certbot_dns_route53.dns_route53:Authenticator', - 'certbot-route53:auth = certbot_dns_route53.dns_route53:Authenticator' + 'certbot-route53:auth = certbot_dns_route53.authenticator:Authenticator' ], }, test_suite='certbot_dns_route53', From 74acd1ee5a0de5264e37abfd818f277786bb22c1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 8 Jun 2017 09:32:41 -0700 Subject: [PATCH 018/125] Release 0.15.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 124 +++++----- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 215 +++++++++++++----- letsencrypt-auto | 124 +++++----- letsencrypt-auto-source/certbot-auto.asc | 14 +- letsencrypt-auto-source/letsencrypt-auto | 26 +-- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/certbot-requirements.txt | 24 +- 19 files changed, 336 insertions(+), 215 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 0938c5c68..02c1822b9 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index ccd7cbc2a..9aba526f8 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.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index c88c96a5b..725c86895 100755 --- a/certbot-auto +++ b/certbot-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.14.2" +LE_AUTO_VERSION="0.15.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -694,7 +694,7 @@ if [ "$1" = "--le-auto-phase2" ]; then # Hashin example: # pip install hashin -# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2 +# hashin -r dependency-requirements.txt cryptography==1.5.2 # sets the new certbot-auto pinned version of cryptography to 1.5.2 argparse==1.4.0 \ @@ -707,6 +707,9 @@ pycparser==2.14 \ --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \ --no-binary pycparser +asn1crypto==0.22.0 \ + --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ + --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a cffi==1.4.2 \ --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ @@ -728,38 +731,42 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -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 +cryptography==1.8.2 \ + --hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \ + --hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \ + --hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \ + --hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \ + --hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \ + --hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \ + --hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \ + --hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \ + --hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \ + --hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \ + --hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \ + --hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \ + --hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \ + --hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \ + --hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \ + --hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \ + --hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \ + --hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \ + --hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \ + --hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \ + --hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \ + --hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \ + --hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \ + --hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \ + --hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \ + --hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 -funcsigs==0.4 \ - --hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \ - --hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033 -idna==2.0 \ - --hash=sha256:9b2fc50bd3c4ba306b9651b69411ef22026d4d8335b93afc2214cef1246ce707 \ - --hash=sha256:16199aad938b290f5be1057c0e1efc6546229391c23cea61ca940c115f7d3d3b +funcsigs==1.0.2 \ + --hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \ + --hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50 +idna==2.5 \ + --hash=sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70 \ + --hash=sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 @@ -768,24 +775,15 @@ linecache2==1.0.0 \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f +packaging==16.8 \ + --hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \ + --hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e parsedatetime==2.1 \ --hash=sha256:ce9d422165cf6e963905cd5f74f274ebf7cc98c941916169178ef93f0e557838 \ --hash=sha256:17c578775520c99131634e09cfca5a05ea9e1bd2a05cd06967ebece10df7af2d 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 @@ -851,27 +849,33 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==1.0.1 \ - --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ - --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +mock==2.0.0 \ + --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ + --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba + +# Contains the requirements for the letsencrypt package. +# +# Since the letsencrypt package depends on certbot and using pip with hashes +# requires that all installed packages have hashes listed, this allows +# dependency-requirements.txt to be used without requiring a hash for a +# (potentially unreleased) Certbot package. + letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.14.2 \ - --hash=sha256:b3068d360beccd3b23a81d7cd2522437d847328811b573a5fe14eb04147667cf \ - --hash=sha256:166b7f4858f5b144b03236b995b787a9da1e410121fb7dcac9c7d3b594bc6fcd -certbot==0.14.2 \ - --hash=sha256:525e15e43c833db9a9934308d69dcdd220fa799488cd84543748671c68aba73d \ - --hash=sha256:5bc8547dcfc0fc587e15253e264f79d8397e48bfbc8697d5aca87eae978769ac -certbot-apache==0.14.2 \ - --hash=sha256:15647d424a5a7e4c44c684324ac07a457a2e0d61fce1acaa421c0b641941a350 \ - --hash=sha256:e5220d3e6ee5114b41b398110dfbd8f13bd1e8c7902758634449e0b4ae515b76 -certbot-nginx==0.14.2 \ - --hash=sha256:231377fbdfb6303adddc73fe3f856b9fb6d0175db825650e39fe3dfd6a58f8ef \ - --hash=sha256:529a18280acf41f5f7e0fe7d82c0a5d4d197d14f82742eaec54bb1d3f69c325a +certbot==0.15.0 \ + --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ + --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb +acme==0.15.0 \ + --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ + --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 +certbot-apache==0.15.0 \ + --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ + --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca +certbot-nginx==0.15.0 \ + --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ + --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index c5ec54e22..f2a2f61e9 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.15.0.dev0' +version = '0.15.0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 26570a0ff..60d8bacd9 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 4849007bc..4d88902bf 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index f76eaa7db..820ba2908 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index bfa89ea25..4d555ab54 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 043a9ded1..225e21f45 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 658961bd8..d7641b70e 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index a138cdea5..3b99856d2 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -3,7 +3,7 @@ import sys from distutils.core import setup from setuptools import find_packages -version = '0.15.0.dev0' +version = '0.15.0' install_requires = [ 'acme=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 4329eb858..920291f3f 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.15.0.dev0' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 5128bf096..96a2ad294 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.15.0.dev0' +__version__ = '0.15.0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 1c46ea2c3..7fc78e108 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -3,26 +3,26 @@ usage: 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. The most common SUBCOMMANDS and flags are: +certificate. The most common SUBCOMMANDS and flags are: 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 + (default) run Obtain & install a certificate in your current webserver + certonly Obtain or renew a certificate, but do not install it + renew Renew all previously obtained certificates that are near expiry + -d DOMAINS Comma-separated list of domains to obtain a certificate for --apache Use the Apache plugin for authentication & installation --standalone Run a standalone webserver for authentication --nginx Use the Nginx plugin for authentication & installation --webroot Place files in a server's webroot folder for authentication - --manual Obtain certs interactively, or using shell script hooks + --manual Obtain certificates interactively, or using shell script hooks -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 + --test-cert Obtain a test certificate from a staging server + --dry-run Test "renew" or "certonly" without saving any certificates to disk manage certificates: - certificates Display information about certs you have from Certbot + certificates Display information about certificates you have from Certbot revoke Revoke a certificate (supply --cert-path) delete Delete a certificate @@ -57,19 +57,19 @@ optional arguments: certificate, specifies the new certificate's name. (default: None) --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' - subcommands. Note: Although --dry-run tries to avoid - making any persistent changes on a system, it is not - completely side-effect free: if used with webserver - authenticator plugins like apache and nginx, it makes - and then reverts temporary config changes in order to - obtain test certs, and reloads webservers to deploy - and then roll back those changes. It also calls --pre- - hook and --post-hook commands if they are defined - because they may be necessary to accurately simulate - renewal. --renew-hook commands are not called. - (default: False) + (invalid) certificates but not saving them to disk. + This can currently only be used with the 'certonly' + and 'renew' subcommands. Note: Although --dry-run + tries to avoid making any persistent changes on a + system, it is not completely side-effect free: if used + with webserver authenticator plugins like apache and + nginx, it makes and then reverts temporary config + changes in order to obtain test certificates, and + reloads webservers to deploy and then roll back those + changes. It also calls --pre-hook and --post-hook + commands if they are defined because they may be + necessary to accurately simulate renewal. --renew-hook + commands are not called. (default: False) --debug-challenges After setting up challenges, wait for user input before submitting to CA (default: False) --preferred-challenges PREF_CHALLS @@ -89,7 +89,7 @@ optional arguments: case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/0.14.2 (certbot; + "". (default: CertbotACMEClient/0.15.0 (certbot; Ubuntu 16.04.2 LTS) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/2.7.12). The flags encoded in the user agent are: --duplicate, --force- @@ -100,11 +100,11 @@ automation: Arguments for automating execution & other tweaks --keep-until-expiring, --keep, --reinstall - 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). (default: Ask) - --expand If an existing cert is a strict subset of the + If the requested certificate matches an existing + certificate, always keep the existing one until it is + due for renewal (for the 'run' subcommand this means + reinstall the existing certificate). (default: Ask) + --expand If an existing certificate is a strict subset of the requested names, always expand and replace it with the additional names. (default: Ask) --version show program's version number and exit @@ -176,8 +176,9 @@ testing: --test-cert, --staging Use the staging server to obtain or revoke test - (invalid) certs; equivalent to --server https://acme- - staging.api.letsencrypt.org/directory (default: False) + (invalid) certificates; 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) @@ -188,25 +189,32 @@ testing: affects the port Certbot listens on. A conforming ACME server will still attempt to connect on port 443. (default: 443) + --tls-sni-01-address TLS_SNI_01_ADDRESS + The address the server listens to during tls-sni-01 + challenge. (default: ) --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) - --break-my-certs Be willing to replace or renew valid certs with - invalid (testing/staging) certs (default: False) + --http-01-address HTTP01_ADDRESS + The address the server listens to during http-01 + challenge. (default: ) + --break-my-certs Be willing to replace or renew valid certificates with + invalid (testing/staging) certificates (default: + False) paths: Arguments changing execution paths & servers --cert-path CERT_PATH - Path to where cert is saved (with auth --csr), + Path to where certificate is saved (with auth --csr), installed from, or revoked. (default: None) - --key-path KEY_PATH Path to private key for cert installation or + --key-path KEY_PATH Path to private key for certificate installation or revocation (if account key is missing) (default: None) --fullchain-path FULLCHAIN_PATH - Accompanying path to a full certificate chain (cert - plus chain). (default: None) + Accompanying path to a full certificate chain + (certificate plus chain). (default: None) --chain-path CHAIN_PATH Accompanying path to a certificate chain. (default: None) @@ -230,10 +238,10 @@ manage: directory run: - Options for obtaining & installing certs + Options for obtaining & installing certificates certonly: - Options for modifying how a cert is obtained + Options for modifying how a certificate is obtained --csr CSR Path to a Certificate Signing Request (CSR) in DER or PEM format. Currently --csr only works with the @@ -272,10 +280,10 @@ renew: the shell variable $RENEWED_LINEAGE will point to the config live subdirectory (for example, "/etc/letsencrypt/live/example.com") containing the - new certs and keys; the shell variable + new certificates and keys; the shell variable $RENEWED_DOMAINS will contain a space-delimited list - of renewed cert domains (for example, "example.com - www.example.com" (default: None) + of renewed certificate domains (for example, + "example.com www.example.com" (default: None) --disable-hook-validation Ordinarily the commands specified for --pre-hook /--post-hook/--renew-hook will be checked for @@ -293,7 +301,7 @@ delete: Options for deleting a certificate revoke: - Options for revocation of certs + Options for revocation of certificates --reason {keycompromise,affiliationchanged,superseded,unspecified,cessationofoperation} Specify reason for revoking certificate. (default: 0) @@ -329,7 +337,7 @@ unregister: --account ACCOUNT_ID Account ID to use (default: None) install: - Options for modifying how a cert is deployed + Options for modifying how a certificate is deployed config_changes: Options for controlling which changes are displayed @@ -352,8 +360,8 @@ plugins: --installers Limit to installer plugins only. (default: None) update_symlinks: - Recreates cert and key symlinks in /etc/letsencrypt/live, if you changed - them by hand or edited a renewal configuration file + Recreates certificate and key symlinks in /etc/letsencrypt/live, if you + changed them by hand or edited a renewal configuration file plugins: Plugin Selection: Certbot client supports an extensible plugins @@ -371,14 +379,30 @@ plugins: -i INSTALLER, --installer INSTALLER Installer plugin name (also used to find domains). (default: None) - --apache Obtain and install certs using Apache (default: False) - --nginx Obtain and install certs using Nginx (default: False) - --standalone Obtain certs using a "standalone" webserver. (default: + --apache Obtain and install certificates using Apache (default: False) - --manual Provide laborious manual instructions for obtaining a - cert (default: False) - --webroot Obtain certs by placing files in a webroot directory. + --nginx Obtain and install certificates using Nginx (default: + False) + --standalone Obtain certificates using a "standalone" webserver. (default: False) + --manual Provide laborious manual instructions for obtaining a + certificate (default: False) + --webroot Obtain certificates by placing files in a webroot + directory. (default: False) + --dns-cloudflare Obtain certificates using a DNS TXT record (if you are + using Cloudflare for DNS). (default: False) + --dns-cloudxns Obtain certificates using a DNS TXT record (if you are + using CloudXNS for DNS). (default: False) + --dns-digitalocean Obtain certificates using a DNS TXT record (if you are + using DigitalOcean for DNS). (default: False) + --dns-dnsimple Obtain certificates using a DNS TXT record (if you are + using DNSimple for DNS). (default: False) + --dns-google Obtain certificates using a DNS TXT record (if you are + using Google Cloud DNS). (default: False) + --dns-nsone Obtain certificates using a DNS TXT record (if you are + using NS1 for DNS). (default: False) + --dns-route53 Obtain certificates using a DNS TXT record (if you are + using Route53 for DNS). (default: False) apache: Apache Web Server plugin - Beta @@ -410,6 +434,95 @@ apache: Let installer handle enabling sites for you.(Only Ubuntu/Debian currently) (default: True) +certbot-route53:auth: + Obtain certificates using a DNS TXT record (if you are using AWS Route53 + for DNS). + + --certbot-route53:auth-propagation-seconds CERTBOT_ROUTE53:AUTH_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 10) + +dns-cloudflare: + Obtain certificates using a DNS TXT record (if you are using Cloudflare + for DNS). + + --dns-cloudflare-propagation-seconds DNS_CLOUDFLARE_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 10) + --dns-cloudflare-credentials DNS_CLOUDFLARE_CREDENTIALS + Cloudflare credentials INI file. (default: None) + +dns-cloudxns: + Obtain certificates using a DNS TXT record (if you are using CloudXNS for + DNS). + + --dns-cloudxns-propagation-seconds DNS_CLOUDXNS_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 30) + --dns-cloudxns-credentials DNS_CLOUDXNS_CREDENTIALS + CloudXNS credentials INI file. (default: None) + +dns-digitalocean: + Obtain certs using a DNS TXT record (if you are using DigitalOcean for + DNS). + + --dns-digitalocean-propagation-seconds DNS_DIGITALOCEAN_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 10) + --dns-digitalocean-credentials DNS_DIGITALOCEAN_CREDENTIALS + DigitalOcean credentials INI file. (default: None) + +dns-dnsimple: + Obtain certificates using a DNS TXT record (if you are using DNSimple for + DNS). + + --dns-dnsimple-propagation-seconds DNS_DNSIMPLE_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 30) + --dns-dnsimple-credentials DNS_DNSIMPLE_CREDENTIALS + DNSimple credentials INI file. (default: None) + +dns-google: + Obtain certificates using a DNS TXT record (if you are using Google Cloud + DNS for DNS). + + --dns-google-propagation-seconds DNS_GOOGLE_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 60) + --dns-google-credentials DNS_GOOGLE_CREDENTIALS + Path to Google Cloud DNS service account JSON file. + (See https://developers.google.com/identity/protocols/ + OAuth2ServiceAccount#creatinganaccount forinformation + about creating a service account and + https://cloud.google.com/dns/access- + control#permissions_and_roles for information about + therequired permissions.) (default: None) + +dns-nsone: + Obtain certificates using a DNS TXT record (if you are using NS1 for DNS). + + --dns-nsone-propagation-seconds DNS_NSONE_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 30) + --dns-nsone-credentials DNS_NSONE_CREDENTIALS + NS1 credentials file. (default: None) + +dns-route53: + Obtain certificates using a DNS TXT record (if you are using AWS Route53 + for DNS). + + --dns-route53-propagation-seconds DNS_ROUTE53_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 10) + manual: Authenticate through manual configuration or custom shell scripts. When using shell scripts, an authenticator script must be provided. The diff --git a/letsencrypt-auto b/letsencrypt-auto index c88c96a5b..725c86895 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.14.2" +LE_AUTO_VERSION="0.15.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -694,7 +694,7 @@ if [ "$1" = "--le-auto-phase2" ]; then # Hashin example: # pip install hashin -# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2 +# hashin -r dependency-requirements.txt cryptography==1.5.2 # sets the new certbot-auto pinned version of cryptography to 1.5.2 argparse==1.4.0 \ @@ -707,6 +707,9 @@ pycparser==2.14 \ --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \ --no-binary pycparser +asn1crypto==0.22.0 \ + --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ + --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a cffi==1.4.2 \ --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ @@ -728,38 +731,42 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -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 +cryptography==1.8.2 \ + --hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \ + --hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \ + --hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \ + --hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \ + --hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \ + --hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \ + --hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \ + --hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \ + --hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \ + --hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \ + --hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \ + --hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \ + --hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \ + --hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \ + --hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \ + --hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \ + --hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \ + --hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \ + --hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \ + --hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \ + --hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \ + --hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \ + --hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \ + --hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \ + --hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \ + --hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 -funcsigs==0.4 \ - --hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \ - --hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033 -idna==2.0 \ - --hash=sha256:9b2fc50bd3c4ba306b9651b69411ef22026d4d8335b93afc2214cef1246ce707 \ - --hash=sha256:16199aad938b290f5be1057c0e1efc6546229391c23cea61ca940c115f7d3d3b +funcsigs==1.0.2 \ + --hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \ + --hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50 +idna==2.5 \ + --hash=sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70 \ + --hash=sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 @@ -768,24 +775,15 @@ linecache2==1.0.0 \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c ordereddict==1.1 \ --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f +packaging==16.8 \ + --hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \ + --hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e parsedatetime==2.1 \ --hash=sha256:ce9d422165cf6e963905cd5f74f274ebf7cc98c941916169178ef93f0e557838 \ --hash=sha256:17c578775520c99131634e09cfca5a05ea9e1bd2a05cd06967ebece10df7af2d 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 @@ -851,27 +849,33 @@ zope.interface==4.1.3 \ --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 -mock==1.0.1 \ - --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ - --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +mock==2.0.0 \ + --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ + --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba + +# Contains the requirements for the letsencrypt package. +# +# Since the letsencrypt package depends on certbot and using pip with hashes +# requires that all installed packages have hashes listed, this allows +# dependency-requirements.txt to be used without requiring a hash for a +# (potentially unreleased) Certbot package. + letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.14.2 \ - --hash=sha256:b3068d360beccd3b23a81d7cd2522437d847328811b573a5fe14eb04147667cf \ - --hash=sha256:166b7f4858f5b144b03236b995b787a9da1e410121fb7dcac9c7d3b594bc6fcd -certbot==0.14.2 \ - --hash=sha256:525e15e43c833db9a9934308d69dcdd220fa799488cd84543748671c68aba73d \ - --hash=sha256:5bc8547dcfc0fc587e15253e264f79d8397e48bfbc8697d5aca87eae978769ac -certbot-apache==0.14.2 \ - --hash=sha256:15647d424a5a7e4c44c684324ac07a457a2e0d61fce1acaa421c0b641941a350 \ - --hash=sha256:e5220d3e6ee5114b41b398110dfbd8f13bd1e8c7902758634449e0b4ae515b76 -certbot-nginx==0.14.2 \ - --hash=sha256:231377fbdfb6303adddc73fe3f856b9fb6d0175db825650e39fe3dfd6a58f8ef \ - --hash=sha256:529a18280acf41f5f7e0fe7d82c0a5d4d197d14f82742eaec54bb1d3f69c325a +certbot==0.15.0 \ + --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ + --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb +acme==0.15.0 \ + --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ + --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 +certbot-apache==0.15.0 \ + --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ + --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca +certbot-nginx==0.15.0 \ + --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ + --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 2650e5922..9c2014502 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v2 -iQEcBAABCAAGBQJZJ0tAAAoJEE0XyZXNl3Xyta0H/3+UZ1xeCc7CjZBMEMjb6IPm -h0KhptkLfwRR0/vGhTeIaOi8rzZYPuzZVwRvTuJ30oORI/zP+siGTOVW4Rt/3KI0 -IZidCJkdl3259jtJpSR9dWOXVp8bklZin8k6daQjbizq8Hl6z0aFLbHlqeSAZhUX -ush94CQwB380OUBut+g3CYx4BxD0dgTODPIaVYzeG8lOX5SXAaBbH79BOAtCr9Hy -sRfYjcBo4aL3rPCayPn+ETvQsYYo/Z7zqHjfShiKzZXNtW+RBGXAf8CGoEk7LKM4 -jts6PxOpg2BFpArDKHn6JIWsHOphBAQ/qIIgvD1mKZj6P4hGBJv4+aZ3Q8uhHeg= -=56Pv +iQEcBAABCAAGBQJZOXvxAAoJEE0XyZXNl3XyAEEH/3S7/j7XNYv3lfy959ef/vFm +xntReU0J9sOpCLPFHmX2/AFSzxc+L70d082+di50uqnTJHEadvVsnb+XURLAhMeD +GmVjrFkAiigvhX3xWIP3Iu9m/r+wp4h2G6Z6duXcNLhra3Z06n2IxD5rIYG118gH +OnVweYhg6e14B4nbVJBQ2XZp2NfIf9RFE973CQk3YC/cMG4Vfj17n6z8e5nuMZhb +Ztxo9YttWhKfGp7BgbWLZaPJ4XE7LETBYVbBT5V5bSl7ktmftXcOwIUgYcevvZqe +1OLLI8KBLW7OUAuKyivfksn/sIaJY1vvKfj+UruqMCuVGwO8lr4BXKBZxMaq5fk= +=mmbE -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index aac67c071..725c86895 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.15.0.dev0" +LE_AUTO_VERSION="0.15.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -864,18 +864,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -acme==0.14.1 \ - --hash=sha256:f535d6459dcafa436749a8d2fdfafed21b792efa05b8bd3263fcd739c2e1497c \ - --hash=sha256:0e6d9d1bbb71d80c61c8d10ab9a40bcf38e25f0fa016b9769e96ebf5a79b552b -certbot==0.14.1 \ - --hash=sha256:f950a058d4f657160de4ad163d9f781fe7adeec0c0a44556841adb03ad135d13 \ - --hash=sha256:519b28124869d97116cb1f2f04ccc2937c0b2fd32fce43576eb80c0e4ff1ab65 -certbot-apache==0.14.1 \ - --hash=sha256:1dda9b4dcf66f6dfba37c787d849e69ad25a344572f74a76fc4447bb1a5417b2 \ - --hash=sha256:da84996e345fc5789da3575225536b27fa3b35f89b2db2d8f494a34bced14f9b -certbot-nginx==0.14.1 \ - --hash=sha256:bd3d4a1dcd6fa9e8ead19a9da88693f08b63464c86be2442e42cd60565c3f05f \ - --hash=sha256:f0c19f667072e4cfa6b92abf8312b6bee3ed1d2432676b211593034e7d1abb7e +certbot==0.15.0 \ + --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ + --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb +acme==0.15.0 \ + --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ + --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 +certbot-apache==0.15.0 \ + --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ + --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca +certbot-nginx==0.15.0 \ + --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ + --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 032f73ca066d20d39221f5e6aa58a2a9af02013c..96ad67971c7681b15b10f0eaf3eb9cdb0b6fa999 100644 GIT binary patch literal 256 zcmV+b0ssD3fX@-nqy*RZKiKGM*9`hKiI~uOv?z@%8e6eoXH#N9%&*n0r(iyCPr(6X z?ZGwB-MR{ATLE6;NnZHM`?|?m8FMwuI{!yQ+!<+ugPHW8uT5dB%z|6H+_M&FlXaTg z_hMunHnrRqT;VQ=xi}bk^BulkXyn(;*#?ps{JJWnCeiiM+3EUC0q`^N@6G;bqF3W% zN0;NrCOOa$BYs{-Q0NkUlWKAtlpd2Ly?c|d6|XfWu}q(Z1&sw$=RJPMqX`pNb&TE` zCU>Eit~$<>R6o*ABsmKrEF#CsN$+V4uO)i^IKukbQgERL>jngxCDs7D(ZI%(;07_T GP?_#C6M&xp literal 256 zcmV+b0ssC3E+vn?mXmvQ;5y9R zngdi#j#+9!W!c9muzY3QGzq(M<pp)vDskpuW|BX~;y4AJ`owMj zp&uQ_Zqo>Zd@?lze5w~4sDB}sl=(yzO9%WbyUB6ZEGjh!Kx*ZA&z`8}@JvHBAj0`~ z;mqv$zd`NgW|%NEI##Y~jd*~VH-;F5ptvUQf1Q-nM|{>(HB>?Nr`h^!P7=8H5B_yM zub@0D>Zqz<*5ZOWuFr9F88LPakTT8F2mo`N4^pfU3T)s)hJY#AT(P%8$aABWuJ|{@ GlUx15cYc@v diff --git a/letsencrypt-auto-source/pieces/certbot-requirements.txt b/letsencrypt-auto-source/pieces/certbot-requirements.txt index 90b47a3cb..da9c33583 100644 --- a/letsencrypt-auto-source/pieces/certbot-requirements.txt +++ b/letsencrypt-auto-source/pieces/certbot-requirements.txt @@ -1,12 +1,12 @@ -acme==0.14.1 \ - --hash=sha256:f535d6459dcafa436749a8d2fdfafed21b792efa05b8bd3263fcd739c2e1497c \ - --hash=sha256:0e6d9d1bbb71d80c61c8d10ab9a40bcf38e25f0fa016b9769e96ebf5a79b552b -certbot==0.14.1 \ - --hash=sha256:f950a058d4f657160de4ad163d9f781fe7adeec0c0a44556841adb03ad135d13 \ - --hash=sha256:519b28124869d97116cb1f2f04ccc2937c0b2fd32fce43576eb80c0e4ff1ab65 -certbot-apache==0.14.1 \ - --hash=sha256:1dda9b4dcf66f6dfba37c787d849e69ad25a344572f74a76fc4447bb1a5417b2 \ - --hash=sha256:da84996e345fc5789da3575225536b27fa3b35f89b2db2d8f494a34bced14f9b -certbot-nginx==0.14.1 \ - --hash=sha256:bd3d4a1dcd6fa9e8ead19a9da88693f08b63464c86be2442e42cd60565c3f05f \ - --hash=sha256:f0c19f667072e4cfa6b92abf8312b6bee3ed1d2432676b211593034e7d1abb7e +certbot==0.15.0 \ + --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ + --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb +acme==0.15.0 \ + --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ + --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 +certbot-apache==0.15.0 \ + --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ + --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca +certbot-nginx==0.15.0 \ + --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ + --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 From 0aab244846e2b97806475679ad962feff48d8f0b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 8 Jun 2017 09:32:57 -0700 Subject: [PATCH 019/125] Bump version to 0.16.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 02c1822b9..76ca87afa 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0' +version = '0.16.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 9aba526f8..64fd8be66 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.15.0' +version = '0.16.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 f2a2f61e9..2b2796704 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.15.0' +version = '0.16.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 60d8bacd9..58444b5c1 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0' +version = '0.16.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 4d88902bf..41282f35c 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0' +version = '0.16.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 820ba2908..13be502e2 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0' +version = '0.16.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 4d555ab54..14f27c960 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0' +version = '0.16.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 225e21f45..ee20fd1fd 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0' +version = '0.16.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index d7641b70e..5c7bb2b5a 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.15.0' +version = '0.16.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 3b99856d2..5f6f61e4e 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -3,7 +3,7 @@ import sys from distutils.core import setup from setuptools import find_packages -version = '0.15.0' +version = '0.16.0.dev0' install_requires = [ 'acme=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 920291f3f..699c6bba6 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.15.0' +version = '0.16.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 96a2ad294..b372841e0 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.15.0' +__version__ = '0.16.0.dev0' diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 725c86895..af5105ab0 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.15.0" +LE_AUTO_VERSION="0.16.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates From 0671b492d677be2f5d287fa66817d701ea593c06 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Wed, 7 Jun 2017 13:53:16 -0700 Subject: [PATCH 020/125] Fix warning in Cloudflare docs --- certbot-dns-cloudflare/docs/api/dns_cloudflare.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-dns-cloudflare/docs/api/dns_cloudflare.rst b/certbot-dns-cloudflare/docs/api/dns_cloudflare.rst index 939d4c0b4..35f525201 100644 --- a/certbot-dns-cloudflare/docs/api/dns_cloudflare.rst +++ b/certbot-dns-cloudflare/docs/api/dns_cloudflare.rst @@ -1,5 +1,5 @@ :mod:`certbot_dns_cloudflare.dns_cloudflare` --------------------------------------- +-------------------------------------------- .. automodule:: certbot_dns_cloudflare.dns_cloudflare :members: From 215c85d7bed0de619de6921ffc45a595c4e9cc12 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Wed, 7 Jun 2017 14:40:55 -0700 Subject: [PATCH 021/125] Provide basic Cloudflare documentation --- .../certbot_dns_cloudflare/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py index f4820e1ca..7a9fa105c 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py @@ -1 +1,17 @@ -"""Cloudflare DNS Authenticator""" +"""Cloudflare DNS Authenticator + +This plugin automates the process of completing a dns-01 challenge +(`~acme.challenges.DNS01`) using the Cloudflare API. + +Use of this plugin requires a configuration file containing Cloudflare API +credentials, obtained from your Cloudflare +`account page `_. + +Example: + +.. code-block:: ini + + dns_cloudflare_email = cloudflare@example.com + dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567 + +""" From 9f7c9decce2fa657aed3f28a8a8286178296c274 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Wed, 7 Jun 2017 15:55:28 -0700 Subject: [PATCH 022/125] Expand documentation --- .../certbot_dns_cloudflare/__init__.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py index 7a9fa105c..6bb22fce2 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py @@ -1,17 +1,35 @@ -"""Cloudflare DNS Authenticator +""" +The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of +completing a `dns-01` challenge (`~acme.challenges.DNS01`) using the Cloudflare +API. -This plugin automates the process of completing a dns-01 challenge -(`~acme.challenges.DNS01`) using the Cloudflare API. + +Credentials +----------- Use of this plugin requires a configuration file containing Cloudflare API credentials, obtained from your Cloudflare `account page `_. -Example: - .. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # Cloudflare API credentials used by Certbot + dns_cloudflare_email = cloudflare@example.com + dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567 + +The path to this file can be provided interactively or using the +`--dns-cloudflare-credentials` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + Cloudflare account. Users who can read this file can use these credentials + to issue API calls on your behalf. Users who can cause Certbot to run using + these credentials can complete a `dns-01` challenge to acquire new + certificates or revoke existing certificates for associated domains. + - dns_cloudflare_email = cloudflare@example.com - dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567 """ From 1817cfe460a48d7d81334b2a7e77e78b0f6ed55a Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Wed, 7 Jun 2017 15:56:09 -0700 Subject: [PATCH 023/125] Document arguments --- .../certbot_dns_cloudflare/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py index 6bb22fce2..779a1002a 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py @@ -4,6 +4,20 @@ completing a `dns-01` challenge (`~acme.challenges.DNS01`) using the Cloudflare API. +Named Arguments +--------------- + +====================================== ======= ============================== +Argument Default Description +====================================== ======= ============================== +`--dns-cloudflare-credentials` None Cloudflare credentials INI + file. (See Credentials_.) +`--dns-cloudflare-propagation-seconds` 10 The number of seconds to wait + for DNS to propagate before + asking the ACME server to + verify the DNS record. +====================================== ======= ============================== + Credentials ----------- From 0387031550dfacf6a5e64d5c709f18db851eb20e Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Wed, 7 Jun 2017 16:32:25 -0700 Subject: [PATCH 024/125] Various improvements --- .../certbot_dns_cloudflare/__init__.py | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py index 779a1002a..dda05185f 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py @@ -1,22 +1,20 @@ """ The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of -completing a `dns-01` challenge (`~acme.challenges.DNS01`) using the Cloudflare -API. +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) using the +Cloudflare API. Named Arguments --------------- -====================================== ======= ============================== -Argument Default Description -====================================== ======= ============================== -`--dns-cloudflare-credentials` None Cloudflare credentials INI - file. (See Credentials_.) -`--dns-cloudflare-propagation-seconds` 10 The number of seconds to wait - for DNS to propagate before - asking the ACME server to - verify the DNS record. -====================================== ======= ============================== +======================================== ===================================== +``--dns-cloudflare-credentials`` Cloudflare credentials_ INI file. + (Required) +``--dns-cloudflare-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 10) +======================================== ===================================== Credentials ----------- @@ -34,16 +32,45 @@ credentials, obtained from your Cloudflare dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567 The path to this file can be provided interactively or using the -`--dns-cloudflare-credentials` command-line argument. Certbot records the path +``--dns-cloudflare-credentials`` command-line argument. Certbot records the path to this file for use during renewal, but does not store the file's contents. .. caution:: You should protect these API credentials as you would the password to your Cloudflare account. Users who can read this file can use these credentials to issue API calls on your behalf. Users who can cause Certbot to run using - these credentials can complete a `dns-01` challenge to acquire new + these credentials can complete a ``dns-01`` challenge to acquire new certificates or revoke existing certificates for associated domains. +Examples +-------- +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-cloudflare \\ + --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-cloudflare \\ + --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-cloudflare \\ + --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\ + --dns-cloudflare-propagation-seconds 60 \\ + -d example.com """ From 1ac7848ce65eeae4328cbaa92ca85b903cda1311 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Wed, 7 Jun 2017 17:02:03 -0700 Subject: [PATCH 025/125] Respond to review feedback * Clarify that the challenge involves TXT records * Clarify potential consequences of credential exposure --- .../certbot_dns_cloudflare/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py index dda05185f..b6a97c762 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py @@ -1,7 +1,7 @@ """ The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of -completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) using the -Cloudflare API. +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the Cloudflare API. Named Arguments @@ -38,9 +38,10 @@ to this file for use during renewal, but does not store the file's contents. .. caution:: You should protect these API credentials as you would the password to your Cloudflare account. Users who can read this file can use these credentials - to issue API calls on your behalf. Users who can cause Certbot to run using - these credentials can complete a ``dns-01`` challenge to acquire new - certificates or revoke existing certificates for associated domains. + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. Examples -------- From ba3b14d4da5daec1c0ee7fba76bee492a384e641 Mon Sep 17 00:00:00 2001 From: Yen Chi Hsuan Date: Fri, 9 Jun 2017 03:08:47 +0800 Subject: [PATCH 026/125] Add Arch Linux constants for Apache (#4466) --- certbot-apache/certbot_apache/constants.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index 7aced5dca..0c5f7263a 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -110,6 +110,24 @@ CLI_DEFAULTS_SUSE = dict( MOD_SSL_CONF_SRC=pkg_resources.resource_filename( "certbot_apache", "options-ssl-apache.conf") ) +CLI_DEFAULTS_ARCH = dict( + server_root="/etc/httpd", + vhost_root="/etc/httpd/conf", + vhost_files="*.conf", + logs_root="/var/log/httpd", + version_cmd=['apachectl', '-v'], + define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'], + restart_cmd=['apachectl', 'graceful'], + conftest_cmd=['apachectl', 'configtest'], + enmod=None, + dismod=None, + le_vhost_ext="-le-ssl.conf", + handle_mods=False, + handle_sites=False, + challenge_location="/etc/httpd/conf", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "certbot_apache", "options-ssl-apache.conf") +) CLI_DEFAULTS = { "default": CLI_DEFAULTS_DEFAULT, "debian": CLI_DEFAULTS_DEBIAN, @@ -125,6 +143,7 @@ CLI_DEFAULTS = { "darwin": CLI_DEFAULTS_DARWIN, "opensuse": CLI_DEFAULTS_SUSE, "suse": CLI_DEFAULTS_SUSE, + "arch": CLI_DEFAULTS_ARCH, } """CLI defaults.""" From 9f56693ad4b7d517fc0db20cfc291116e91d646f Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 12:18:56 -0700 Subject: [PATCH 027/125] Add documentation for CloudXNS --- .../certbot_dns_cloudxns/__init__.py | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py index 8df02d0fa..7260612cd 100644 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py +++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py @@ -1 +1,77 @@ -"""CloudXNS DNS Authenticator""" +""" +The `~certbot_dns_cloudxns.dns_cloudxns` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the CloudXNS API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-cloudxns-credentials` ` CloudXNS credentials_ INI file. + (Required) +``--dns-cloudxns-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +======================================== ===================================== + +Credentials +----------- + +Use of this plugin requires a configuration file containing CloudXNS API +credentials, obtained from your CloudXNS +`API page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # CloudXNS API credentials used by Certbot + dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef + dns_cloudxns_secret_key = 1122334455667788 + +The path to this file can be provided interactively or using the +``--dns-cloudxns-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + CloudXNS account. Users who can read this file can use these credentials to + issue arbitrary API calls on your behalf. Users who can cause Certbot to run + using these credentials can complete a ``dns-01`` challenge to acquire new + certificates or revoke existing certificates for associated domains, even if + those domains aren't being managed by this server. + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-cloudxns \\ + --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-cloudxns \\ + --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-cloudxns \\ + --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ + --dns-cloudxns-propagation-seconds 60 \\ + -d example.com + +""" From 650611bd1f2a3500e27da7c94ae99608250d95b2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 8 Jun 2017 12:22:46 -0700 Subject: [PATCH 028/125] Fix oldest tests (#4782) * Add pip_install_editable.sh * add install_and_test.sh * simplify tox.ini and fix oldest tests * Put paths & packages on their own line in tox.ini --- tools/install_and_test.sh | 21 +++++ tools/pip_install_editable.sh | 10 +++ tox.ini | 154 ++++++++++++++++------------------ 3 files changed, 102 insertions(+), 83 deletions(-) create mode 100755 tools/install_and_test.sh create mode 100755 tools/pip_install_editable.sh diff --git a/tools/install_and_test.sh b/tools/install_and_test.sh new file mode 100755 index 000000000..0de2ea3f8 --- /dev/null +++ b/tools/install_and_test.sh @@ -0,0 +1,21 @@ +#!/bin/sh -e +# pip installs the requested packages in editable mode and runs unit tests on +# them. Each package is installed and tested in the order they are provided +# before the script moves on to the next package. If CERTBOT_NO_PIN is set not +# set to 1, packages are installed using certbot-auto's requirements file as +# constraints. + +if [ "$CERTBOT_NO_PIN" = 1 ]; then + pip_install="pip install -e" +else + pip_install="$(dirname $0)/pip_install_editable.sh" +fi + +for requirement in "$@" ; do + $pip_install $requirement + pkg=$(echo $requirement | cut -f1 -d\[) # remove any extras such as [dev] + if [ $pkg = "." ]; then + pkg="certbot" + fi + nosetests -v $pkg --processes=-1 --process-timeout=100 +done diff --git a/tools/pip_install_editable.sh b/tools/pip_install_editable.sh new file mode 100755 index 000000000..6130bf6e7 --- /dev/null +++ b/tools/pip_install_editable.sh @@ -0,0 +1,10 @@ +#!/bin/sh -e +# pip installs packages in editable mode using certbot-auto's requirements file +# as constraints + +args="" +for requirement in "$@" ; do + args="$args -e $requirement" +done + +"$(dirname $0)/pip_install.sh" $args diff --git a/tox.ini b/tox.ini index 8279a687f..6db60c882 100644 --- a/tox.ini +++ b/tox.ini @@ -10,102 +10,90 @@ envlist = modification,py{26,33,34,35,36},cover,lint # loops, especially on Travis [base] -# wraps pip install to use pinned versions of dependencies -pip_install = {toxinidir}/tools/pip_install.sh -# packages installed separately to ensure that downstream deps problems -# are detected, c.f. #1002 -core_commands = - {[base]pip_install} -e acme[dev] - nosetests -v acme --processes=-1 - {[base]pip_install} -e .[dev] - nosetests -v certbot --processes=-1 --process-timeout=100 -core_install_args = -e acme[dev] -e .[dev] -core_paths = acme/acme certbot +# pip installs the requested packages in editable mode +pip_install = {toxinidir}/tools/pip_install_editable.sh +# pip installs the requested packages in editable mode and runs unit tests on +# them. Each package is installed and tested in the order they are provided +# before the script moves on to the next package. If CERTBOT_NO_PIN is set not +# set to 1, packages are installed using certbot-auto's requirements file as +# constraints. +install_and_test = {toxinidir}/tools/install_and_test.sh +py26_packages = + acme[dev] \ + .[dev] \ + certbot-apache \ + certbot-dns-cloudflare \ + certbot-dns-digitalocean \ + certbot-dns-google \ + certbot-dns-route53 \ + certbot-nginx \ + letshelp-certbot +non_py26_packages = + certbot-dns-cloudxns \ + certbot-dns-dnsimple \ + certbot-dns-nsone +all_packages = + {[base]py26_packages} {[base]non_py26_packages} +install_packages = + {toxinidir}/tools/pip_install_editable.sh {[base]all_packages} +source_paths = + acme/acme + certbot + certbot-apache/certbot_apache + certbot-compatibility-test/certbot_compatibility_test + certbot-dns-cloudflare/certbot_dns_cloudflare + certbot-dns-cloudxns/certbot_dns_cloudxns + certbot-dns-digitalocean/certbot_dns_digitalocean + certbot-dns-dnsimple/certbot_dns_dnsimple + certbot-dns-google/certbot_dns_google + certbot-dns-nsone/certbot_dns_nsone + certbot-dns-route53/certbot_dns_route53 + certbot-nginx/certbot_nginx + letshelp-certbot/letshelp_certbot + tests/lock_test.py -plugin_commands = - {[base]pip_install} -e certbot-apache - nosetests -v certbot_apache --processes=-1 --process-timeout=80 - {[base]pip_install} -e certbot-nginx - nosetests -v certbot_nginx --processes=-1 -plugin_install_args = -e certbot-apache -e certbot-nginx -plugin_paths = certbot-apache/certbot_apache certbot-nginx/certbot_nginx - -dns_plugin_commands = - pip install -e certbot-dns-cloudflare - nosetests -v certbot_dns_cloudflare --processes=-1 - pip install -e certbot-dns-digitalocean - nosetests -v certbot_dns_digitalocean --processes=-1 - pip install -e certbot-dns-google - nosetests -v certbot_dns_google --processes=-1 - pip install -e certbot-dns-route53 - nosetests -v certbot_dns_route53 --processes=-1 --process-timeout=25 -dns_plugin_install_args = -e certbot-dns-cloudflare -e certbot-dns-digitalocean -e certbot-dns-google -e certbot-dns-route53 -dns_plugin_paths = certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-google/certbot_dns_google certbot-dns-route53/certbot_dns_route53 - -lexicon_dns_plugin_commands = - pip install -e certbot-dns-cloudxns - nosetests -v certbot_dns_cloudxns --processes=-1 - pip install -e certbot-dns-dnsimple - nosetests -v certbot_dns_dnsimple --processes=-1 - pip install -e certbot-dns-nsone - nosetests -v certbot_dns_nsone --processes=-1 -lexicon_dns_plugin_install_args = -e certbot-dns-cloudxns -e certbot-dns-dnsimple -e certbot-dns-nsone -lexicon_dns_plugin_paths = certbot-dns-cloudxns/certbot_dns_cloudxns certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-nsone/certbot_dns_nsone - -compatibility_install_args = -e certbot-compatibility-test -compatibility_paths = certbot-compatibility-test/certbot_compatibility_test - -other_commands = - {[base]pip_install} -e letshelp-certbot - nosetests -v letshelp_certbot --processes=-1 - python tests/lock_test.py -other_install_args = -e letshelp-certbot -other_paths = letshelp-certbot/letshelp_certbot tests/lock_test.py - -[testenv] +[testenv:py26] commands = - {[base]core_commands} - {[base]plugin_commands} - {[base]dns_plugin_commands} - {[base]lexicon_dns_plugin_commands} - {[base]other_commands} - -setenv = - PYTHONPATH = {toxinidir} - PYTHONHASHSEED = 0 -# https://testrun.org/tox/latest/example/basic.html#special-handling-of-pythonhas + {[base]install_and_test} {[base]py26_packages} + python tests/lock_test.py +[testenv:py26-oldest] +commands = + {[testenv:py26]commands} # 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 oldest tests due to # https://github.com/shazow/urllib3/pull/930 deps = - py{26,27}-oldest: cffi<=1.7 - py{26,27}-oldest: cryptography==1.2 - py{26,27}-oldest: configargparse==0.10.0 - py{26,27}-oldest: PyOpenSSL==0.13 - py{26,27}-oldest: requests<=2.11.1 + cffi<=1.7 + cryptography==1.2 + configargparse==0.10.0 + PyOpenSSL==0.13 + requests<=2.11.1 +setenv = + CERTBOT_NO_PIN=1 -[testenv:py26] -commands = - {[base]core_commands} - {[base]plugin_commands} - {[base]dns_plugin_commands} - {[base]other_commands} - -[testenv:py26-oldest] +[testenv] commands = {[testenv:py26]commands} + {[base]install_and_test} {[base]non_py26_packages} +setenv = + PYTHONPATH = {toxinidir} + PYTHONHASHSEED = 0 + py27-oldest: {[testenv:py26-oldest]setenv} +# https://testrun.org/tox/latest/example/basic.html#special-handling-of-pythonhas +deps = + py27-oldest: {[testenv:py26-oldest]deps} [testenv:py27_install] basepython = python2.7 commands = - {[base]pip_install} {[base]core_install_args} {[base]plugin_install_args} {[base]dns_plugin_install_args} {[base]lexicon_dns_plugin_install_args} {[base]other_install_args} + {[base]install_packages} [testenv:cover] basepython = python2.7 commands = - {[base]pip_install} {[base]core_install_args} {[base]plugin_install_args} {[base]dns_plugin_install_args} {[base]lexicon_dns_plugin_install_args} {[base]other_install_args} + {[base]install_packages} ./tox.cover.sh [testenv:lint] @@ -115,25 +103,25 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - {[base]pip_install} -q {[base]core_install_args} {[base]plugin_install_args} {[base]dns_plugin_install_args} {[base]lexicon_dns_plugin_install_args} {[base]compatibility_install_args} {[base]other_install_args} - pylint --reports=n --rcfile=.pylintrc {[base]core_paths} {[base]plugin_paths} {[base]dns_plugin_paths} {[base]lexicon_dns_plugin_paths} {[base]compatibility_paths} {[base]other_paths} + {[base]install_packages} + pylint --reports=n --rcfile=.pylintrc {[base]source_paths} [testenv:mypy] basepython = python3.4 commands = {[base]pip_install} mypy - {[base]pip_install} -q {[base]core_install_args} {[base]plugin_install_args} {[base]dns_plugin_install_args} {[base]lexicon_dns_plugin_install_args} {[base]compatibility_install_args} {[base]other_install_args} - mypy --py2 --ignore-missing-imports {[base]core_paths} {[base]plugin_paths} {[base]dns_plugin_paths} {[base]lexicon_dns_plugin_paths} {[base]compatibility_paths} {[base]other_paths} + {[base]install_packages} + mypy --py2 --ignore-missing-imports {[base]source_paths} [testenv:apacheconftest] #basepython = python2.7 commands = - {[base]pip_install} {[base]core_install_args} {[base]plugin_install_args} {[base]compatibility_install_args} {[base]other_install_args} + {[base]pip_install} acme . certbot-apache certbot-compatibility-test {toxinidir}/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test --debian-modules [testenv:nginxroundtrip] commands = - {[base]pip_install} {[base]core_install_args} -e certbot-nginx + {[base]pip_install} acme . certbot-apache 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 From 502ea82ac432385bf4cb321866d261a4b18db42b Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 13:58:53 -0700 Subject: [PATCH 029/125] Add documentation for DigitalOcean --- .../certbot_dns_digitalocean/__init__.py | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py index 40b2527f8..7565a3725 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py @@ -1 +1,77 @@ -"""DigitalOcean DNS Authenticator""" +""" +The `~certbot_dns_digitalocean.dns_digitalocean` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the DigitalOcean API. + + +Named Arguments +--------------- + +========================================== =================================== +``--dns-digitalocean-credentials`` DigitalOcean credentials_ INI file. + (Required) +``--dns-digitalocean-propagation-seconds`` The number of seconds to wait for + DNS to propagate before asking the + ACME server to verify the DNS + record. + (Default: 10) +========================================== =================================== + +Credentials +----------- + +Use of this plugin requires a configuration file containing DigitalOcean API +credentials, obtained from your DigitalOcean account's `Applications & API +Tokens page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # DigitalOcean API credentials used by Certbot + dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff + +The path to this file can be provided interactively or using the +``--dns-digitalocean-credentials`` command-line argument. Certbot records the +path to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + DigitalOcean account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-digitalocean \\ + --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-digitalocean \\ + --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-digitalocean \\ + --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\ + --dns-digitalocean-propagation-seconds 60 \\ + -d example.com + +""" From 0325ad9244835fac724126f918b049a145f6cdb7 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 14:18:52 -0700 Subject: [PATCH 030/125] Add documentation for DNSimple --- .../certbot_dns_dnsimple/__init__.py | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py b/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py index 1d6747249..1f5a6cbe4 100644 --- a/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py +++ b/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py @@ -1 +1,76 @@ -"""DNSimple DNS Authenticator""" +""" +The `~certbot_dns_dnsimple.dns_dnsimple` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the DNSimple API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-dnsimple-credentials`` DNSimple credentials_ INI file. + (Required) +``--dns-dnsimple-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +======================================== ===================================== + +Credentials +----------- + +Use of this plugin requires a configuration file containing DNSimple API +credentials, obtained from your DNSimple +`account page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # DNSimple API credentials used by Certbot + dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw + +The path to this file can be provided interactively or using the +``--dns-dnsimple-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + DNSimple account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-dnsimple \\ + --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-dnsimple \\ + --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-dnsimple \\ + --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\ + --dns-dnsimple-propagation-seconds 60 \\ + -d example.com + +""" From c180a1065f3b526a209446767ff8fe18690499f0 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 15:08:11 -0700 Subject: [PATCH 031/125] Add documentation for Google --- .../certbot_dns_google/__init__.py | 90 ++++++++++++++++++- certbot-dns-google/docs/conf.py | 7 +- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/certbot-dns-google/certbot_dns_google/__init__.py b/certbot-dns-google/certbot_dns_google/__init__.py index 9e9096d83..362c6d8ce 100644 --- a/certbot-dns-google/certbot_dns_google/__init__.py +++ b/certbot-dns-google/certbot_dns_google/__init__.py @@ -1 +1,89 @@ -"""Google Cloud DNS Authenticator""" +""" +The `~certbot_dns_google.dns_google` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the Google Cloud DNS API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-google-credentials`` Google Cloud Platform credentials_ + JSON file. + (Required) +``--dns-google-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 60) +======================================== ===================================== + +Credentials +----------- + +Use of this plugin requires a configuration file containing Google Cloud +Platform API credentials for an account with the following permissions: + +* ``dns.changes.create`` +* ``dns.changes.get`` +* ``dns.managedZones.list`` +* ``dns.resourceRecordSets.create`` +* ``dns.resourceRecordSets.delete`` + +Google provides instructions for +`creating a service account `_ +and +`information about the required permissions `_. + +.. code-block:: json + :name: credentials.json + :caption: Example credentials file: + + { + "type": "service_account", + ... + } + +The path to this file can be provided interactively or using the +``--dns-google-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would a password. Users who + can read this file can use these credentials to issue some types of API calls + on your behalf, limited by the permissions assigned to the account. Users who + can cause Certbot to run using these credentials can complete a ``dns-01`` + challenge to acquire new certificates or revoke existing certificates for + domains these credentials are authorized to manage. + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-google \\ + --dns-google-credentials ~/.secrets/certbot/google.json \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-google \\ + --dns-google-credentials ~/.secrets/certbot/google.json \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 120 seconds + for DNS propagation + + certbot certonly \\ + --dns-google \\ + --dns-google-credentials ~/.secrets/certbot/google.ini \\ + --dns-google-propagation-seconds 120 \\ + -d example.com + +""" diff --git a/certbot-dns-google/docs/conf.py b/certbot-dns-google/docs/conf.py index 4ff1af1d1..bbb343ee8 100644 --- a/certbot-dns-google/docs/conf.py +++ b/certbot-dns-google/docs/conf.py @@ -17,8 +17,8 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import sys +sys.path.insert(0, os.path.abspath('_ext')) # -- General configuration ------------------------------------------------ @@ -34,7 +34,8 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', - 'sphinx.ext.viewcode'] + 'sphinx.ext.viewcode', + 'jsonlexer'] autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance', 'private-members'] From d570bf5f2df690be0b4b541649b3cace53451f48 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 8 Jun 2017 15:25:22 -0700 Subject: [PATCH 032/125] Update CHANGELOG.md for 0.15.0 (#4803) * Update CHANGELOG.md for 0.15.0 * mention #4575 in CHANGELOG.md --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d52518195..a0f196ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,65 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). +## 0.15.0 - 2017-06-08 + +### Added + +* Plugins for performing DNS challenges for popular providers. Like the Apache + and Nginx plugins, these plugins are packaged separately and not included in + Certbot by default. So far, we have plugins for + [Amazon Route 53](https://pypi.python.org/pypi/certbot-dns-route53), + [Cloudflare](https://pypi.python.org/pypi/certbot-dns-cloudflare), + [DigitalOcean](https://pypi.python.org/pypi/certbot-dns-digitalocean), and + [Google Cloud](https://pypi.python.org/pypi/certbot-dns-google) which all + work on Python 2.6, 2.7, and 3.3+. Additionally, we have plugins for + [CloudXNS](https://pypi.python.org/pypi/certbot-dns-cloudxns), + [DNSimple](https://pypi.python.org/pypi/certbot-dns-dnsimple), + [NS1](https://pypi.python.org/pypi/certbot-dns-nsone) which work on Python + 2.7 and 3.3+ (and not 2.6). Currently, there isn't a good way to install + these plugins when using `certbot-auto`, but that should change soon. +* IPv6 support in the standalone plugin. When performing a challenge, the + standalone plugin automatically handles listening for IPv4/IPv6 traffic based + on the configuration of your system. +* A mechanism for keeping your Apache and Nginx SSL/TLS configuration up to + date. When the Apache or Nginx plugins are used, they place SSL/TLS + configuration options in the root of Certbot's config directory + (`/etc/letsencrypt` by default). Now when a new version of these plugins run + on your system, they will automatically update the file to the newest + version if it is unmodified. If you manually modified the file, Certbot will + display a warning giving you a path to the updated file which you can use as + a reference to manually update your modified copy. +* `--http-01-address` and `--tls-sni-01-address` flags for controlling the + address Certbot listens on when using the standalone plugin. +* The command `certbot certificates` that lists certificates managed by Certbot + now performs additional validity checks to notify you if your files have + become corrupted. + +### Changed + +* Messages custom hooks print to `stdout` are now displayed by Certbot when not + running in `--quiet` mode. +* `jwk` and `alg` fields in JWS objects have been moved into the protected + header causing Certbot to more closely follow the latest version of the ACME + spec. + +### Fixed + +* Permissions on renewal configuration files are now properly preserved when + they are updated. +* A bug causing Certbot to display strange defaults in its help output when + using Python <= 2.7.4 has been fixed. +* Certbot now properly handles mixed case domain names found in custom CSRs. +* A number of poorly worded prompts and error messages. + +### Removed + +* Support for OpenSSL 1.0.0 in `certbot-auto` has been removed as we now pin a + newer version of `cryptography` which dropped support for this version. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.15.0+is%3Aclosed + ## 0.14.2 - 2017-05-25 ### Fixed From 14b1d2d72b2ea0c1d10ac9d5b81dd60e8aa5acc4 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 15:44:38 -0700 Subject: [PATCH 033/125] Explicitly mention the permissions warning and chmod 600 --- .../certbot_dns_cloudflare/__init__.py | 9 +++++++++ certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py | 9 +++++++++ .../certbot_dns_digitalocean/__init__.py | 9 +++++++++ certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py | 9 +++++++++ certbot-dns-google/certbot_dns_google/__init__.py | 9 +++++++++ 5 files changed, 45 insertions(+) diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py index b6a97c762..7e53f83ce 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py @@ -16,6 +16,7 @@ Named Arguments (Default: 10) ======================================== ===================================== + Credentials ----------- @@ -43,6 +44,14 @@ to this file for use during renewal, but does not store the file's contents. new certificates or revoke existing certificates for associated domains, even if those domains aren't being managed by this server. +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + Examples -------- diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py index 7260612cd..6957b9cc3 100644 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py +++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py @@ -16,6 +16,7 @@ Named Arguments (Default: 30) ======================================== ===================================== + Credentials ----------- @@ -43,6 +44,14 @@ to this file for use during renewal, but does not store the file's contents. certificates or revoke existing certificates for associated domains, even if those domains aren't being managed by this server. +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + Examples -------- diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py index 7565a3725..3ab8df041 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/__init__.py @@ -17,6 +17,7 @@ Named Arguments (Default: 10) ========================================== =================================== + Credentials ----------- @@ -43,6 +44,14 @@ path to this file for use during renewal, but does not store the file's contents new certificates or revoke existing certificates for associated domains, even if those domains aren't being managed by this server. +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + Examples -------- diff --git a/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py b/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py index 1f5a6cbe4..f8a2e83aa 100644 --- a/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py +++ b/certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py @@ -16,6 +16,7 @@ Named Arguments (Default: 30) ======================================== ===================================== + Credentials ----------- @@ -42,6 +43,14 @@ to this file for use during renewal, but does not store the file's contents. new certificates or revoke existing certificates for associated domains, even if those domains aren't being managed by this server. +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + Examples -------- diff --git a/certbot-dns-google/certbot_dns_google/__init__.py b/certbot-dns-google/certbot_dns_google/__init__.py index 362c6d8ce..228adafcf 100644 --- a/certbot-dns-google/certbot_dns_google/__init__.py +++ b/certbot-dns-google/certbot_dns_google/__init__.py @@ -17,6 +17,7 @@ Named Arguments (Default: 60) ======================================== ===================================== + Credentials ----------- @@ -55,6 +56,14 @@ to this file for use during renewal, but does not store the file's contents. challenge to acquire new certificates or revoke existing certificates for domains these credentials are authorized to manage. +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + Examples -------- From 25d6369b04e9de0c9498af737d65c7aaeeec1cc3 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 15:51:47 -0700 Subject: [PATCH 034/125] Add documentation for NS1 --- .../certbot_dns_nsone/__init__.py | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/certbot-dns-nsone/certbot_dns_nsone/__init__.py b/certbot-dns-nsone/certbot_dns_nsone/__init__.py index 8c061edf7..e59be74a7 100644 --- a/certbot-dns-nsone/certbot_dns_nsone/__init__.py +++ b/certbot-dns-nsone/certbot_dns_nsone/__init__.py @@ -1 +1,85 @@ -"""NS1 DNS Authenticator""" +""" +The `~certbot_dns_nsone.dns_nsone` plugin automates the process of completing +a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently +removing, TXT records using the NS1 API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-nsone-credentials`` NS1 credentials_ INI file. + (Required) +``--dns-nsone-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +======================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing NS1 API credentials, +obtained from your NS1 +`account page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # NS1 API credentials used by Certbot + dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw + +The path to this file can be provided interactively or using the +``--dns-nsone-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + NS1 account. Users who can read this file can use these credentials to issue + arbitrary API calls on your behalf. Users who can cause Certbot to run using + these credentials can complete a ``dns-01`` challenge to acquire new + certificates or revoke existing certificates for associated domains, even if + those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-nsone \\ + --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-nsone \\ + --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-nsone \\ + --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\ + --dns-nsone-propagation-seconds 60 \\ + -d example.com + +""" From de7e55688fc895c6e6bab4eb02d18fdc91fcba45 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 15:56:38 -0700 Subject: [PATCH 035/125] fixup! Add documentation for Google --- certbot-dns-google/docs/_ext/jsonlexer.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 certbot-dns-google/docs/_ext/jsonlexer.py diff --git a/certbot-dns-google/docs/_ext/jsonlexer.py b/certbot-dns-google/docs/_ext/jsonlexer.py new file mode 100644 index 000000000..1ad004d2b --- /dev/null +++ b/certbot-dns-google/docs/_ext/jsonlexer.py @@ -0,0 +1,16 @@ +"""Copied from https://stackoverflow.com/a/16863232""" + +def setup(app): + # enable Pygments json lexer + try: + import pygments + if pygments.__version__ >= '1.5': + # use JSON lexer included in recent versions of Pygments + from pygments.lexers import JsonLexer + else: + # use JSON lexer from pygments-json if installed + from pygson.json_lexer import JSONLexer as JsonLexer + except ImportError: + pass # not fatal if we have old (or no) Pygments and no pygments-json + else: + app.add_lexer('json', JsonLexer()) From c353fd349aacc37427d3779572c580d8041bd03a Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 16:13:13 -0700 Subject: [PATCH 036/125] fixup! Add documentation for Google --- certbot-dns-google/certbot_dns_google/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/certbot-dns-google/certbot_dns_google/__init__.py b/certbot-dns-google/certbot_dns_google/__init__.py index 228adafcf..a3667ca2a 100644 --- a/certbot-dns-google/certbot_dns_google/__init__.py +++ b/certbot-dns-google/certbot_dns_google/__init__.py @@ -30,10 +30,10 @@ Platform API credentials for an account with the following permissions: * ``dns.resourceRecordSets.create`` * ``dns.resourceRecordSets.delete`` -Google provides instructions for -`creating a service account `_ -and -`information about the required permissions `_. +Google provides instructions for `creating a service account `_ and +`information about the required permissions `_. .. code-block:: json :name: credentials.json @@ -96,3 +96,4 @@ Examples -d example.com """ +# pylint: disable=line-too-long From 5ee47e921038a37228fbe2567cecdf5a9be3df31 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 8 Jun 2017 16:19:30 -0700 Subject: [PATCH 037/125] fixup! Add documentation for CloudXNS --- certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py index 6957b9cc3..6ddbdfe5a 100644 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py +++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py @@ -8,7 +8,7 @@ Named Arguments --------------- ======================================== ===================================== -``--dns-cloudxns-credentials` ` CloudXNS credentials_ INI file. +``--dns-cloudxns-credentials`` CloudXNS credentials_ INI file. (Required) ``--dns-cloudxns-propagation-seconds`` The number of seconds to wait for DNS to propagate before asking the ACME From 76ecb7035f72ac759caa14a4257cd8fc36eee474 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 9 Jun 2017 08:35:04 -0700 Subject: [PATCH 038/125] Remove "alpha" and "beta" qualifiers from docs. (#4808) --- README.rst | 4 ++-- docs/cli-help.txt | 4 ++-- docs/using.rst | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index ff6fafe36..b96421e7a 100644 --- a/README.rst +++ b/README.rst @@ -132,8 +132,8 @@ Current Features * Supports multiple web servers: - - apache/2.x (beta support for auto-configuration) - - nginx/0.8.48+ (alpha support for auto-configuration, beta support in 0.14.0) + - apache/2.x + - nginx/0.8.48+ - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - standalone (runs its own simple webserver to prove you control a domain) diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 7fc78e108..4265056ce 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -405,7 +405,7 @@ plugins: using Route53 for DNS). (default: False) apache: - Apache Web Server plugin - Beta + Apache Web Server plugin --apache-enmod APACHE_ENMOD Path to the Apache 'a2enmod' binary. (default: @@ -544,7 +544,7 @@ manual: Automatically allows public IP logging (default: Ask) nginx: - Nginx Web Server plugin - Alpha + Nginx Web Server plugin --nginx-server-root NGINX_SERVER_ROOT Nginx server root directory. (default: /etc/nginx) diff --git a/docs/using.rst b/docs/using.rst index 4ef0f5414..45f303188 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -49,7 +49,7 @@ apache_ Y Y | Automates obtaining and installing a certificate with Ap webroot_ Y N | Obtains a certificate by writing to the webroot directory of http-01_ (80) | an already running webserver. nginx_ Y Y | Automates obtaining and installing a certificate with Nginx. tls-sni-01_ (443) - | Alpha release shipped with Certbot 0.9.0. + | Shipped with Certbot 0.9.0. standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. http-01_ (80) or | Requires port 80 or 443 to be available. This is useful on tls-sni-01_ (443) | systems with no webserver, or when direct integration with @@ -132,7 +132,7 @@ Nginx ----- 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 +work for most configurations. 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. From 228726597bc65ceff1bdd75e9221ad6674ae825f Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Fri, 9 Jun 2017 12:04:24 -0500 Subject: [PATCH 039/125] Update Arch package name for acme (#4811) We have migrated to use Python 3 variant of acme, so let's list python-acme instead of the old python2-acme one. --- docs/packaging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/packaging.rst b/docs/packaging.rst index 3b7e21ab3..bd31f8c1f 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -43,7 +43,7 @@ Arch From our official releases: -- https://www.archlinux.org/packages/community/any/python2-acme +- https://www.archlinux.org/packages/community/any/python-acme - https://www.archlinux.org/packages/community/any/certbot - https://www.archlinux.org/packages/community/any/certbot-apache - https://www.archlinux.org/packages/community/any/certbot-nginx From cd34c4272dec8c14ecfecd73a9a66678a0de1a2e Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 9 Jun 2017 10:09:45 -0700 Subject: [PATCH 040/125] Improve text of manual plugin. (#4810) --- certbot/plugins/manual.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 25e2564fa..093fec4be 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -44,22 +44,14 @@ Please deploy a DNS TXT record under the name Before continuing, verify the record is deployed.""" _HTTP_INSTRUCTIONS = """\ -Make sure your web server displays the following content at -{uri} before continuing: +Create a file containing just this data: {validation} -If you don't have HTTP server configured, you can run the following -command on the target server (as root): +And make it available on your web server at this URL: -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()" """ +{uri} +""" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) From 1871f0d1b9b3955f52ed541b716662f77e72a0d1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 9 Jun 2017 10:46:51 -0700 Subject: [PATCH 041/125] Deduplicate code using textwrap.fill. --- certbot/display/util.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 5b01dd8d4..df80789d7 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -176,12 +176,8 @@ class FileDisplay(object): if self._return_default(message, default, cli_flag, force_interactive): return OK, default - ans = input_with_timeout( - textwrap.fill( - "%s (Enter 'c' to cancel): " % message, - 80, - break_long_words=False, - break_on_hyphens=False)) + message = _wrap_lines("%s (Enter 'c' to cancel): " % message) + ans = input_with_timeout(message) if ans == "c" or ans == "C": return CANCEL, "-1" @@ -392,12 +388,8 @@ class FileDisplay(object): # Write out the menu choices for i, desc in enumerate(choices, 1): - self.outfile.write( - textwrap.fill( - "{num}: {desc}".format(num=i, desc=desc), - 80, - break_long_words=False, - break_on_hyphens=False)) + msg = "{num}: {desc}".format(num=i, desc=desc) + self.outfile.write(_wrap_lines(msg)) # Keep this outside of the textwrap self.outfile.write(os.linesep) From 2ebd8e976344a93bca11e1aae63a93f11366fc66 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 9 Jun 2017 10:50:21 -0700 Subject: [PATCH 042/125] Add trailing space to prompt. --- certbot/display/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index df80789d7..f1922ebb9 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -176,7 +176,8 @@ class FileDisplay(object): if self._return_default(message, default, cli_flag, force_interactive): return OK, default - message = _wrap_lines("%s (Enter 'c' to cancel): " % message) + # Trailing space must be added outside of _wrap_lines to be preserved + message = _wrap_lines("%s (Enter 'c' to cancel):" % message) + " " ans = input_with_timeout(message) if ans == "c" or ans == "C": From 9610320ee75a5ec28e61874c71f97f5c91346888 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 9 Jun 2017 11:57:31 -0700 Subject: [PATCH 043/125] remove #certbot@OFTC from using.rst (#4812) --- docs/using.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 45f303188..4bbf81d50 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -750,9 +750,8 @@ Getting help 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) `_. +You can also chat with us on IRC: `(#letsencrypt @ +freenode) `_ If you find a bug in the software, please do report it in our `issue tracker `_. Remember to From 3b3c878b117b7d62f27fa818d282c1fe68cd8a76 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Fri, 9 Jun 2017 12:06:49 -0700 Subject: [PATCH 044/125] fixup! fixup! Add documentation for Google --- certbot-dns-google/certbot_dns_google/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/certbot-dns-google/certbot_dns_google/__init__.py b/certbot-dns-google/certbot_dns_google/__init__.py index a3667ca2a..26685206c 100644 --- a/certbot-dns-google/certbot_dns_google/__init__.py +++ b/certbot-dns-google/certbot_dns_google/__init__.py @@ -96,4 +96,3 @@ Examples -d example.com """ -# pylint: disable=line-too-long From d3549e18a708bbe8e416937c0d71caf6a6bf43ba Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 9 Jun 2017 12:48:59 -0700 Subject: [PATCH 045/125] Correct message about vhost ambiguity. When our Apache plugin is unable to determine which virtual host to use in non-interactive mode, it raises an error about vhost ambiguity with instructions on how to fix the problem. These instructions stated that we require one vhost per file which is no longer accurate since #4706 so I removed this part of the error message. --- certbot-apache/certbot_apache/display_ops.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index 6bcb64dd5..7578accd4 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -88,10 +88,11 @@ def _vhost_menu(domain, vhosts): 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 " - "in its own conf file, and may need vhosts to be explicitly " - "labelled with ServerName or ServerAlias directives.") + msg = ( + "Encountered vhost ambiguity but unable to ask for user " + "guidance in non-interactive mode. Certbot may need " + "vhosts to be explicitly labelled with ServerName or " + "ServerAlias directives.") logger.warning(msg) raise errors.MissingCommandlineFlag(msg) From 31d5d278873cbf34ec6afd0f46796e2154a6461b Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Fri, 9 Jun 2017 13:42:37 -0700 Subject: [PATCH 046/125] route53: add module-level documentation Add module-level documentation describing the use of certbot-dns-route53 including discussion of credential management. --- .../certbot_dns_route53/__init__.py | 121 +++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/certbot-dns-route53/certbot_dns_route53/__init__.py b/certbot-dns-route53/certbot_dns_route53/__init__.py index c91c79c22..8659617ef 100644 --- a/certbot-dns-route53/certbot_dns_route53/__init__.py +++ b/certbot-dns-route53/certbot_dns_route53/__init__.py @@ -1 +1,120 @@ -"""Certbot Route53 plugin.""" +""" +The `~certbot_dns_route53.dns_route53` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the Amazon Web Services Route 53 API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-route53-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 10) +======================================== ===================================== + + +Credentials +----------- +Use of this plugin requires a configuration file containing Amazon Web Sevices +API credentials for an account with the following permissions: + +* ``route53:ListHostedZones`` +* ``route53:GetChange`` +* ``route53:ChangeResourceRecordSets`` + +These permissions can be captured in an AWS policy like the one below. Amazon +provides `information about managing access `_ and `information about +the required permissions `_ + +.. code-block:: json + :name: sample-aws-policy.json + :caption: Example AWS policy file: + + { + "Version": "2012-10-17", + "Id": "certbot-dns-route53 sample policy", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "route53:ListHostedZones", + "route53:GetChange" + ], + "Resource": [ + "*" + ] + }, + { + "Effect" : "Allow", + "Action" : [ + "route53:ChangeResourceRecordSets" + ], + "Resource" : [ + "arn:aws:route53:::hostedzone/YOURHOSTEDZONEID" + ] + } + ] + } + +The `access keys `_ for an account +with these permissions must be supplied in one of the following ways, which are +discussed in more detail in the Boto3 library's documentation about `configuring +credentials `_. + +* Using the ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environment + variables. +* Using a credentials configuration file at the default location, + ``~/.aws/config``. +* Using a credentials configuration file at a path supplied using the + ``AWS_CONFIG_FILE`` environment variable. + +.. code-block:: ini + :name: config.ini + :caption: Example credentials config file: + + [default] + aws_access_key_id=AKIAIOSFODNN7EXAMPLE + aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + +.. caution:: + You should protect these API credentials as you would a password. Users who + can read this file can use these credentials to issue some types of API calls + on your behalf, limited by the permissions assigned to the account. Users who + can cause Certbot to run using these credentials can complete a ``dns-01`` + challenge to acquire new certificates or revoke existing certificates for + domains these credentials are authorized to manage. + + +Examples +-------- +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-route53 \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-route53 \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 30 seconds + for DNS propagation + + certbot certonly \\ + --dns-route53 \\ + --dns-route53-propagation-seconds 30 \\ + -d example.com +""" From a04f63bd31bae7583d34e47ec97c376cf394a201 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Fri, 9 Jun 2017 16:01:59 -0700 Subject: [PATCH 047/125] route53: autogenerate documentation (#4816) Creates documentation boilerplate for certbot-dns-route53 using tools/sphinx-quickstart.sh. Adds trivial `authenticator.rst` and `dns_route53.rst` files. --- certbot-dns-route53/MANIFEST.in | 1 + certbot-dns-route53/docs/.gitignore | 1 + certbot-dns-route53/docs/Makefile | 20 ++ certbot-dns-route53/docs/api.rst | 8 + .../docs/api/authenticator.rst | 5 + certbot-dns-route53/docs/api/dns_route53.rst | 5 + certbot-dns-route53/docs/conf.py | 180 ++++++++++++++++++ certbot-dns-route53/docs/index.rst | 28 +++ certbot-dns-route53/docs/make.bat | 36 ++++ 9 files changed, 284 insertions(+) create mode 100644 certbot-dns-route53/docs/.gitignore create mode 100644 certbot-dns-route53/docs/Makefile create mode 100644 certbot-dns-route53/docs/api.rst create mode 100644 certbot-dns-route53/docs/api/authenticator.rst create mode 100644 certbot-dns-route53/docs/api/dns_route53.rst create mode 100644 certbot-dns-route53/docs/conf.py create mode 100644 certbot-dns-route53/docs/index.rst create mode 100644 certbot-dns-route53/docs/make.bat diff --git a/certbot-dns-route53/MANIFEST.in b/certbot-dns-route53/MANIFEST.in index 9575a1c62..c48c07e59 100644 --- a/certbot-dns-route53/MANIFEST.in +++ b/certbot-dns-route53/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE include README +recursive-include docs * diff --git a/certbot-dns-route53/docs/.gitignore b/certbot-dns-route53/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-route53/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-route53/docs/Makefile b/certbot-dns-route53/docs/Makefile new file mode 100644 index 000000000..25be9609c --- /dev/null +++ b/certbot-dns-route53/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = certbot-dns-route53 +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/certbot-dns-route53/docs/api.rst b/certbot-dns-route53/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-route53/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-route53/docs/api/authenticator.rst b/certbot-dns-route53/docs/api/authenticator.rst new file mode 100644 index 000000000..2d96a419b --- /dev/null +++ b/certbot-dns-route53/docs/api/authenticator.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_route53.authenticator` +---------------------------------------- + +.. automodule:: certbot_dns_route53.authenticator + :members: diff --git a/certbot-dns-route53/docs/api/dns_route53.rst b/certbot-dns-route53/docs/api/dns_route53.rst new file mode 100644 index 000000000..7573f2e19 --- /dev/null +++ b/certbot-dns-route53/docs/api/dns_route53.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_route53.dns_route53` +-------------------------------------- + +.. automodule:: certbot_dns_route53.dns_route53 + :members: diff --git a/certbot-dns-route53/docs/conf.py b/certbot-dns-route53/docs/conf.py new file mode 100644 index 000000000..25a7c6e4d --- /dev/null +++ b/certbot-dns-route53/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-route53 documentation build configuration file, created by +# sphinx-quickstart on Fri Jun 9 11:45:30 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-route53' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-route53doc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-route53.tex', u'certbot-dns-route53 Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-route53', u'certbot-dns-route53 Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-route53', u'certbot-dns-route53 Documentation', + author, 'certbot-dns-route53', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-route53/docs/index.rst b/certbot-dns-route53/docs/index.rst new file mode 100644 index 000000000..bacf73150 --- /dev/null +++ b/certbot-dns-route53/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-route53 documentation master file, created by + sphinx-quickstart on Fri Jun 9 11:45:30 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-route53's documentation! +=============================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_route53 + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-route53/docs/make.bat b/certbot-dns-route53/docs/make.bat new file mode 100644 index 000000000..e92b5909a --- /dev/null +++ b/certbot-dns-route53/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-route53 + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd From 498f8ad75ca6d677bd8898453300e1e6c60a731c Mon Sep 17 00:00:00 2001 From: Johannes Keyser Date: Mon, 12 Jun 2017 16:40:56 +0200 Subject: [PATCH 048/125] Add AAAA record check suggestion (#4824) --- certbot/auth_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index a1f23a895..5f520cbcb 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -444,7 +444,7 @@ def _report_no_chall_path(): _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 " + "correctly and the DNS A/AAAA record(s) for that domain contain(s) the " "right IP address.") From 6d74a0d3ce50906723b4453254bdd6a83aa49073 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Mon, 12 Jun 2017 09:41:35 -0500 Subject: [PATCH 049/125] Fix digitalocean plugin tests for Python 3 (#4821) assertItemsEqual() doesn't exist in Python 3.x. Travis didn't fail because of some nose errors (so not all tests were run). --- .../certbot_dns_digitalocean/dns_digitalocean_test.py | 5 +++-- certbot-dns-digitalocean/setup.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py index 7e97eed07..11c8c57d5 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean_test.py @@ -5,6 +5,7 @@ import unittest import digitalocean import mock +import six from certbot import errors from certbot.plugins import dns_test_common @@ -133,8 +134,8 @@ class DigitalOceanClientTest(unittest.TestCase): correct_record_mock.destroy.assert_called() - self.assertItemsEqual(first_record_mock.destroy.call_args_list, []) - self.assertItemsEqual(last_record_mock.destroy.call_args_list, []) + six.assertCountEqual(self, first_record_mock.destroy.call_args_list, []) + six.assertCountEqual(self, last_record_mock.destroy.call_args_list, []) def test_del_txt_record_error_finding_domain(self): self.manager.get_all_domains.side_effect = API_ERROR diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 13be502e2..6b689f660 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -15,6 +15,7 @@ install_requires = [ # For pkg_resources. >=1.0 so pip resolves it to a version cryptography # will tolerate; see #2599: 'setuptools>=1.0', + 'six', 'zope.interface', ] From aab097bf7f791344b3dfe437850542ce4aa94711 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Mon, 12 Jun 2017 09:41:50 -0500 Subject: [PATCH 050/125] Add DNS Authenticator plugins (#4822) --- docs/packaging.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/packaging.rst b/docs/packaging.rst index bd31f8c1f..cc43ee71a 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -11,6 +11,13 @@ We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.python.org/pypi/certbot - https://pypi.python.org/pypi/certbot-apache - https://pypi.python.org/pypi/certbot-nginx +- https://pypi.python.org/pypi/certbot-dns-cloudflare +- https://pypi.python.org/pypi/certbot-dns-cloudxns +- https://pypi.python.org/pypi/certbot-dns-digitalocean +- https://pypi.python.org/pypi/certbot-dns-dnsimple +- https://pypi.python.org/pypi/certbot-dns-google +- https://pypi.python.org/pypi/certbot-dns-nsone +- https://pypi.python.org/pypi/certbot-dns-route53 The following scripts are used in the process: @@ -47,6 +54,13 @@ From our official releases: - https://www.archlinux.org/packages/community/any/certbot - https://www.archlinux.org/packages/community/any/certbot-apache - https://www.archlinux.org/packages/community/any/certbot-nginx +- https://www.archlinux.org/packages/community/any/certbot-dns-cloudflare +- https://www.archlinux.org/packages/community/any/certbot-dns-cloudxns +- https://www.archlinux.org/packages/community/any/certbot-dns-digitalocean +- https://www.archlinux.org/packages/community/any/certbot-dns-dnsimple +- https://www.archlinux.org/packages/community/any/certbot-dns-google +- https://www.archlinux.org/packages/community/any/certbot-dns-nsone +- https://www.archlinux.org/packages/community/any/certbot-dns-route53 From ``master``: https://aur.archlinux.org/packages/certbot-git From cafd4802b7104f4b74a551fa8bf14f00e6a8f421 Mon Sep 17 00:00:00 2001 From: Andrew Ittner Date: Mon, 12 Jun 2017 08:12:41 -0700 Subject: [PATCH 051/125] Add more useful usage instructions for all subcommands (#4710) * Add more useful usage instructions for all subcommands Fixes #3875. * Update usage instructions Address PR comments. Fixes #3875. * Fix line length * Suffix usage lines with two empty lines Per review, this brackets all usage text in 2 newlines to match existing text. --- certbot/cli.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index a74b50636..533acb4b2 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -357,7 +357,8 @@ VERB_HELP = [ }), ("delete", { "short": "Clean up all files related to a certificate", - "opts": "Options for deleting a certificate" + "opts": "Options for deleting a certificate", + "usage": "\n\n certbot delete --cert-name CERTNAME\n\n" }), ("revoke", { "short": "Revoke a certificate specified with --cert-path", @@ -366,33 +367,41 @@ VERB_HELP = [ }), ("register", { "short": "Register for account with Let's Encrypt / other ACME server", - "opts": "Options for account registration & modification" + "opts": "Options for account registration & modification", + "usage": "\n\n certbot register --email user@example.com [options]\n\n" }), ("unregister", { "short": "Irrevocably deactivate your account", - "opts": "Options for account deactivation." + "opts": "Options for account deactivation.", + "usage": "\n\n certbot unregister [options]\n\n" }), ("install", { "short": "Install an arbitrary certificate in a server", - "opts": "Options for modifying how a certificate is deployed" + "opts": "Options for modifying how a certificate is deployed", + "usage": "\n\n certbot install --cert-path /path/to/fullchain.pem " + " --key-path /path/to/private-key [options]\n\n" }), ("config_changes", { "short": "Show changes that Certbot has made to server configurations", - "opts": "Options for controlling which changes are displayed" + "opts": "Options for controlling which changes are displayed", + "usage": "\n\n certbot config_changes --num NUM [options]\n\n" }), ("rollback", { "short": "Roll back server conf changes made during certificate installation", - "opts": "Options for rolling back server configuration changes" + "opts": "Options for rolling back server configuration changes", + "usage": "\n\n certbot rollback --checkpoints 3 [options]\n\n" }), ("plugins", { "short": "List plugins that are installed and available on your system", - "opts": 'Options for for the "plugins" subcommand' + "opts": 'Options for for the "plugins" subcommand', + "usage": "\n\n certbot plugins [options]\n\n" }), ("update_symlinks", { "short": "Recreate symlinks in your /etc/letsencrypt/live/ directory", "opts": ("Recreates certificate 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"))) + os.path.join(flag_default("config_dir"), "live"))), + "usage": "\n\n certbot update_symlinks [options]\n\n" }), ] From 4866e9fe61f5204794b6cf5bce97ddd507b47b2f Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Mon, 12 Jun 2017 10:20:51 -0500 Subject: [PATCH 052/125] Fix a typo: syncronization -> synchronization (#4826) --- certbot/tests/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/util.py b/certbot/tests/util.py index 76e3d5846..a36f0f6ac 100644 --- a/certbot/tests/util.py +++ b/certbot/tests/util.py @@ -276,7 +276,7 @@ def lock_and_call(func, lock_path): def hold_lock(cv, lock_path): # pragma: no cover """Acquire a file lock at lock_path and wait to release it. - :param multiprocessing.Condition cv: condition for syncronization + :param multiprocessing.Condition cv: condition for synchronization :param str lock_path: path to the file lock """ From 98805ccae01205537cbfa48dfa3704982fc128dc Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Mon, 12 Jun 2017 16:50:21 -0400 Subject: [PATCH 053/125] Upgrade cryptography to 1.9 for certbot-auto. Fix #4640. (#4815) * Upgrade cryptography to 1.9 for certbot-auto. Fix #4640. * Update cffi to the latest, as cryptography now requires >=1.7. --- letsencrypt-auto-source/letsencrypt-auto | 100 ++++++++++-------- .../pieces/dependency-requirements.txt | 100 ++++++++++-------- 2 files changed, 112 insertions(+), 88 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index af5105ab0..3e5db44ff 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -710,54 +710,66 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -cffi==1.4.2 \ - --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ - --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ - --hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \ - --hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \ - --hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \ - --hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \ - --hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \ - --hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \ - --hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \ - --hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \ - --hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \ - --hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \ - --hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \ - --hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \ - --hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \ - --hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998 +cffi==1.10.0 \ + --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ + --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ + --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ + --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ + --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ + --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ + --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ + --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ + --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ + --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ + --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ + --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ + --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ + --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ + --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ + --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ + --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ + --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ + --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ + --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ + --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ + --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ + --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ + --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ + --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ + --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ + --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ + --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ + --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ + --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ + --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ + --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ + --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ + --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ + --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ + --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.8.2 \ - --hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \ - --hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \ - --hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \ - --hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \ - --hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \ - --hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \ - --hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \ - --hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \ - --hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \ - --hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \ - --hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \ - --hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \ - --hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \ - --hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \ - --hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \ - --hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \ - --hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \ - --hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \ - --hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \ - --hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \ - --hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \ - --hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \ - --hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \ - --hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \ - --hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \ - --hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda +cryptography==1.9 \ + --hash=sha256:469a9d3d851038f1eb7d7f77bb08bb4775b41483372be450e25b293fe57bd59e \ + --hash=sha256:533143321d15c8743f91eec5c5f495c1b5cad9a25de8f6dab01eddd6b416903e \ + --hash=sha256:2230c186182d773064d06242e0fa604cd718bfff28aa9c5ae73d7e426e98a151 \ + --hash=sha256:3dc94ed5a26b8553a325767358f505c0a43e0c89df078647f77a4d022ddcdc57 \ + --hash=sha256:2eb8297b877cb6b56216750fa7017c9f5786bec8afd6a0f1aaace02cbfb6195f \ + --hash=sha256:025a96e48164106f2082b00f42bf430cf21f09e203e42585a712e420b75cbff0 \ + --hash=sha256:61eb3534f8ed2415dd708b28919205d523f220e4cd5b8165844edfdd4a649b8e \ + --hash=sha256:5474fe5ce6b517d3086e0231b6ad88f8978c551c4379f91c3d619c308490f0d7 \ + --hash=sha256:5ff169869624e23767d70db274f13a9ea4e97c029425a1224aa5e049e84ce2af \ + --hash=sha256:c26e057a2de13e97e708328d295c5ac4cd3eab4a5c42ce727dd1a53316034b8a \ + --hash=sha256:7db719432648f14ea33edffc5f75330c595804eac86ca916528b35ce50a8bfd6 \ + --hash=sha256:ca72537a7064bb80e34b6908e576ffc8e2c2cad29a7168f48d0999df089695c3 \ + --hash=sha256:2a5e577f5d2093e51486b4ec02b51bb5adb165b98fee99929d5af0813e90b469 \ + --hash=sha256:9d9da8bac2e31003d092f5ef6981a725c77c4e9a30638436884d61ad39f9a1ee \ + --hash=sha256:fab8ec6866e384d9827d5dc02a1383e991a6c05c54f818316c4b829e56ca2ba3 \ + --hash=sha256:365eb804362e581c9a02e7a610b30514f07dd77b2a38aed338eb5192446bbc58 \ + --hash=sha256:2908f709f02711dbb10561a9f154d2f7d1792385e2341e9708539cc4ecfb8667 \ + --hash=sha256:5518337022718029e367d982642f3e3523541e098ad671672a90b82474c84882 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt index 807c1c812..a8007ba3e 100644 --- a/letsencrypt-auto-source/pieces/dependency-requirements.txt +++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt @@ -21,54 +21,66 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -cffi==1.4.2 \ - --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ - --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ - --hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \ - --hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \ - --hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \ - --hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \ - --hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \ - --hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \ - --hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \ - --hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \ - --hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \ - --hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \ - --hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \ - --hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \ - --hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \ - --hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998 +cffi==1.10.0 \ + --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ + --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ + --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ + --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ + --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ + --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ + --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ + --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ + --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ + --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ + --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ + --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ + --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ + --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ + --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ + --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ + --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ + --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ + --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ + --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ + --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ + --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ + --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ + --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ + --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ + --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ + --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ + --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ + --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ + --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ + --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ + --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ + --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ + --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ + --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ + --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.8.2 \ - --hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \ - --hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \ - --hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \ - --hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \ - --hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \ - --hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \ - --hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \ - --hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \ - --hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \ - --hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \ - --hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \ - --hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \ - --hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \ - --hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \ - --hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \ - --hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \ - --hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \ - --hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \ - --hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \ - --hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \ - --hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \ - --hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \ - --hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \ - --hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \ - --hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \ - --hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda +cryptography==1.9 \ + --hash=sha256:469a9d3d851038f1eb7d7f77bb08bb4775b41483372be450e25b293fe57bd59e \ + --hash=sha256:533143321d15c8743f91eec5c5f495c1b5cad9a25de8f6dab01eddd6b416903e \ + --hash=sha256:2230c186182d773064d06242e0fa604cd718bfff28aa9c5ae73d7e426e98a151 \ + --hash=sha256:3dc94ed5a26b8553a325767358f505c0a43e0c89df078647f77a4d022ddcdc57 \ + --hash=sha256:2eb8297b877cb6b56216750fa7017c9f5786bec8afd6a0f1aaace02cbfb6195f \ + --hash=sha256:025a96e48164106f2082b00f42bf430cf21f09e203e42585a712e420b75cbff0 \ + --hash=sha256:61eb3534f8ed2415dd708b28919205d523f220e4cd5b8165844edfdd4a649b8e \ + --hash=sha256:5474fe5ce6b517d3086e0231b6ad88f8978c551c4379f91c3d619c308490f0d7 \ + --hash=sha256:5ff169869624e23767d70db274f13a9ea4e97c029425a1224aa5e049e84ce2af \ + --hash=sha256:c26e057a2de13e97e708328d295c5ac4cd3eab4a5c42ce727dd1a53316034b8a \ + --hash=sha256:7db719432648f14ea33edffc5f75330c595804eac86ca916528b35ce50a8bfd6 \ + --hash=sha256:ca72537a7064bb80e34b6908e576ffc8e2c2cad29a7168f48d0999df089695c3 \ + --hash=sha256:2a5e577f5d2093e51486b4ec02b51bb5adb165b98fee99929d5af0813e90b469 \ + --hash=sha256:9d9da8bac2e31003d092f5ef6981a725c77c4e9a30638436884d61ad39f9a1ee \ + --hash=sha256:fab8ec6866e384d9827d5dc02a1383e991a6c05c54f818316c4b829e56ca2ba3 \ + --hash=sha256:365eb804362e581c9a02e7a610b30514f07dd77b2a38aed338eb5192446bbc58 \ + --hash=sha256:2908f709f02711dbb10561a9f154d2f7d1792385e2341e9708539cc4ecfb8667 \ + --hash=sha256:5518337022718029e367d982642f3e3523541e098ad671672a90b82474c84882 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 From 22ee81bdfd2077381620092cbdd467581bd47976 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 15 Jun 2017 14:57:06 -0700 Subject: [PATCH 054/125] fix sphinx build failures (#4831) --- docs/index.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index d2399d5d8..746080864 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,12 +16,6 @@ Welcome to the Certbot documentation! api -.. toctree:: - :hidden: - - challenges - ciphers - Indices and tables ================== From efe5b4c82f1b02fe2fdd2bdfbf90f227e6d88619 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 15 Jun 2017 15:33:42 -0700 Subject: [PATCH 055/125] Pin zope.interface in oldest tests (#4842) * pin zope.interface in oldest tests * pin zope.component in oldest tests --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 6db60c882..8c9aaaf26 100644 --- a/tox.ini +++ b/tox.ini @@ -70,6 +70,8 @@ deps = configargparse==0.10.0 PyOpenSSL==0.13 requests<=2.11.1 + zope.component==4.0.2 + zope.interface==4.0.5 setenv = CERTBOT_NO_PIN=1 From 0a269f31d007b8331297d21ab9c33e932cb4101b Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 15 Jun 2017 16:41:00 -0700 Subject: [PATCH 056/125] DNS Made Easy DNS Authenticator (#4603) Implement an Authenticator which can fulfill a dns-01 challenge using the DNS Made Easy API. Applicable only for domains using DNS Made Easy. Testing Done: * `tox -e py27` * `tox -e lint` * Manual testing: (`http://api.sandbox.dnsmadeeasy.com/V2.0` used as the `api_endpoint` for all manual testing) * Used `certbot certonly --dns-dnsmadeeasy -d`, specifying a credentials file as a command line argument. Verified that a certificate was successfully obtained without user interaction. * Negative testing: * Path to non-existent credentials file. * Credentials file with unsafe permissions (644). * Path to credentials file with an invalid API key. * Path to credentials file with a malformed API key. * Path to credentials file with an invalid Secret key. * Path to credentials file with a malformed Secret key. * Domain name not registered to DNS Made Easy account. --- certbot-dns-dnsmadeeasy/LICENSE.txt | 190 ++++++++++++++++++ certbot-dns-dnsmadeeasy/MANIFEST.in | 3 + certbot-dns-dnsmadeeasy/README.rst | 1 + .../certbot_dns_dnsmadeeasy/__init__.py | 1 + .../dns_dnsmadeeasy.py | 89 ++++++++ .../dns_dnsmadeeasy_test.py | 56 ++++++ certbot-dns-dnsmadeeasy/docs/.gitignore | 1 + certbot-dns-dnsmadeeasy/docs/Makefile | 20 ++ certbot-dns-dnsmadeeasy/docs/api.rst | 8 + .../docs/api/dns_dnsmadeeasy.rst | 5 + certbot-dns-dnsmadeeasy/docs/conf.py | 180 +++++++++++++++++ certbot-dns-dnsmadeeasy/docs/index.rst | 28 +++ certbot-dns-dnsmadeeasy/docs/make.bat | 36 ++++ certbot-dns-dnsmadeeasy/setup.cfg | 2 + certbot-dns-dnsmadeeasy/setup.py | 68 +++++++ certbot/cli.py | 3 + certbot/plugins/disco.py | 1 + certbot/plugins/selection.py | 8 +- tools/venv.sh | 1 + tools/venv3.sh | 1 + tox.cover.sh | 4 +- tox.ini | 2 + 22 files changed, 704 insertions(+), 4 deletions(-) create mode 100644 certbot-dns-dnsmadeeasy/LICENSE.txt create mode 100644 certbot-dns-dnsmadeeasy/MANIFEST.in create mode 100644 certbot-dns-dnsmadeeasy/README.rst create mode 100644 certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py create mode 100644 certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy.py create mode 100644 certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy_test.py create mode 100644 certbot-dns-dnsmadeeasy/docs/.gitignore create mode 100644 certbot-dns-dnsmadeeasy/docs/Makefile create mode 100644 certbot-dns-dnsmadeeasy/docs/api.rst create mode 100644 certbot-dns-dnsmadeeasy/docs/api/dns_dnsmadeeasy.rst create mode 100644 certbot-dns-dnsmadeeasy/docs/conf.py create mode 100644 certbot-dns-dnsmadeeasy/docs/index.rst create mode 100644 certbot-dns-dnsmadeeasy/docs/make.bat create mode 100644 certbot-dns-dnsmadeeasy/setup.cfg create mode 100644 certbot-dns-dnsmadeeasy/setup.py diff --git a/certbot-dns-dnsmadeeasy/LICENSE.txt b/certbot-dns-dnsmadeeasy/LICENSE.txt new file mode 100644 index 000000000..981c46c9f --- /dev/null +++ b/certbot-dns-dnsmadeeasy/LICENSE.txt @@ -0,0 +1,190 @@ + 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/certbot-dns-dnsmadeeasy/MANIFEST.in b/certbot-dns-dnsmadeeasy/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-dnsmadeeasy/README.rst b/certbot-dns-dnsmadeeasy/README.rst new file mode 100644 index 000000000..4456914ce --- /dev/null +++ b/certbot-dns-dnsmadeeasy/README.rst @@ -0,0 +1 @@ +DNS Made Easy DNS Authenticator plugin for Certbot diff --git a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py new file mode 100644 index 000000000..c8b78d3cf --- /dev/null +++ b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py @@ -0,0 +1 @@ +"""DNS Made Easy DNS Authenticator""" diff --git a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy.py b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy.py new file mode 100644 index 000000000..982edfdd3 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy.py @@ -0,0 +1,89 @@ +"""DNS Authenticator for DNS Made Easy DNS.""" +import logging + +import zope.interface +from lexicon.providers import dnsmadeeasy + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common +from certbot.plugins import dns_common_lexicon + +logger = logging.getLogger(__name__) + +ACCOUNT_URL = 'https://cp.dnsmadeeasy.com/account/info' + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for DNS Made Easy + + This Authenticator uses the DNS Made Easy API to fulfill a dns-01 challenge. + """ + + description = ('Obtain certificates using a DNS TXT record (if you are using DNS Made Easy for ' + 'DNS).') + ttl = 60 + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.credentials = None + + @classmethod + def add_parser_arguments(cls, add): # pylint: disable=arguments-differ + super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60) + add('credentials', help='DNS Made Easy credentials INI file.') + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ + 'the DNS Made Easy API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'DNS Made Easy credentials INI file', + { + 'api-key': 'API key for DNS Made Easy account, obtained from {0}' + .format(ACCOUNT_URL), + 'secret-key': 'Secret key for DNS Made Easy account, obtained from {0}' + .format(ACCOUNT_URL) + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_dnsmadeeasy_client().add_txt_record(domain, validation_name, validation) + + def _cleanup(self, domain, validation_name, validation): + self._get_dnsmadeeasy_client().del_txt_record(domain, validation_name, validation) + + def _get_dnsmadeeasy_client(self): + return _DNSMadeEasyLexiconClient(self.credentials.conf('api-key'), + self.credentials.conf('secret-key'), + self.ttl) + + +class _DNSMadeEasyLexiconClient(dns_common_lexicon.LexiconClient): + """ + Encapsulates all communication with the DNS Made Easy via Lexicon. + """ + + def __init__(self, api_key, secret_key, ttl): + super(_DNSMadeEasyLexiconClient, self).__init__() + + self.provider = dnsmadeeasy.Provider({ + 'auth_username': api_key, + 'auth_token': secret_key, + 'ttl': ttl, + }) + + def _handle_http_error(self, e, domain_name): + if domain_name in str(e) and str(e).startswith('404 Client Error: Not Found for url:'): + return + + hint = None + if str(e).startswith('403 Client Error: Forbidden for url:'): + hint = 'Are your API key and Secret key values correct?' + + return errors.PluginError('Error determining zone identifier: {0}.{1}' + .format(e, ' ({0})'.format(hint) if hint else '')) diff --git a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy_test.py b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy_test.py new file mode 100644 index 000000000..44a777e1b --- /dev/null +++ b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/dns_dnsmadeeasy_test.py @@ -0,0 +1,56 @@ +"""Tests for certbot_dns_dnsmadeeasy.dns_dnsmadeeasy.""" + +import os +import unittest + +import mock +from requests.exceptions import HTTPError + +from certbot.plugins import dns_test_common +from certbot.plugins import dns_test_common_lexicon +from certbot.plugins.dns_test_common import DOMAIN +from certbot.tests import util as test_util + +API_KEY = 'foo' +SECRET_KEY = 'bar' + + +class AuthenticatorTest(test_util.TempDirTestCase, + dns_test_common_lexicon.BaseLexiconAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_dnsmadeeasy.dns_dnsmadeeasy import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write({"dnsmadeeasy_api_key": API_KEY, + "dnsmadeeasy_secret_key": SECRET_KEY}, + path) + + self.config = mock.MagicMock(dnsmadeeasy_credentials=path, + dnsmadeeasy_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "dnsmadeeasy") + + self.mock_client = mock.MagicMock() + # _get_dnsmadeeasy_client | pylint: disable=protected-access + self.auth._get_dnsmadeeasy_client = mock.MagicMock(return_value=self.mock_client) + + +class DNSMadeEasyLexiconClientTest(unittest.TestCase, + dns_test_common_lexicon.BaseLexiconClientTest): + DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN)) + LOGIN_ERROR = HTTPError('403 Client Error: Forbidden for url: {0}.'.format(DOMAIN)) + + def setUp(self): + from certbot_dns_dnsmadeeasy.dns_dnsmadeeasy import _DNSMadeEasyLexiconClient + + self.client = _DNSMadeEasyLexiconClient(API_KEY, SECRET_KEY, 0) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-dnsmadeeasy/docs/.gitignore b/certbot-dns-dnsmadeeasy/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-dnsmadeeasy/docs/Makefile b/certbot-dns-dnsmadeeasy/docs/Makefile new file mode 100644 index 000000000..9b46118d1 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = certbot-dns-dnsmadeeasy +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/certbot-dns-dnsmadeeasy/docs/api.rst b/certbot-dns-dnsmadeeasy/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-dnsmadeeasy/docs/api/dns_dnsmadeeasy.rst b/certbot-dns-dnsmadeeasy/docs/api/dns_dnsmadeeasy.rst new file mode 100644 index 000000000..81948a77f --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/api/dns_dnsmadeeasy.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_dnsmadeeasy.dns_dnsmadeeasy` +------------------------------------ + +.. automodule:: certbot_dns_dnsmadeeasy.dns_dnsmadeeasy + :members: diff --git a/certbot-dns-dnsmadeeasy/docs/conf.py b/certbot-dns-dnsmadeeasy/docs/conf.py new file mode 100644 index 000000000..7d26f9742 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-dnsmadeeasy documentation build configuration file, created by +# sphinx-quickstart on Wed May 10 18:39:34 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-dnsmadeeasy' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-dnsmadeeasydoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-dnsmadeeasy.tex', u'certbot-dns-dnsmadeeasy Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-dnsmadeeasy', u'certbot-dns-dnsmadeeasy Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-dnsmadeeasy', u'certbot-dns-dnsmadeeasy Documentation', + author, 'certbot-dns-dnsmadeeasy', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-dnsmadeeasy/docs/index.rst b/certbot-dns-dnsmadeeasy/docs/index.rst new file mode 100644 index 000000000..2e9aef36b --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-dnsmadeeasy documentation master file, created by + sphinx-quickstart on Wed May 10 18:39:34 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-dnsmadeeasy's documentation! +=================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_dnsmadeeasy + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-dnsmadeeasy/docs/make.bat b/certbot-dns-dnsmadeeasy/docs/make.bat new file mode 100644 index 000000000..f204c8393 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-dnsmadeeasy + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-dnsmadeeasy/setup.cfg b/certbot-dns-dnsmadeeasy/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-dnsmadeeasy/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py new file mode 100644 index 000000000..f3b9b8151 --- /dev/null +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -0,0 +1,68 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.16.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme=={0}'.format(version), + 'certbot=={0}'.format(version), + 'dns-lexicon', + 'mock', + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-dnsmadeeasy', + version=version, + description="DNS Made Easy DNS Authenticator plugin for Certbot", + url='https://github.com/certbot/certbot', + 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.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + '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, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-dnsmadeeasy = certbot_dns_dnsmadeeasy.dns_dnsmadeeasy:Authenticator', + ], + }, + test_suite='certbot_dns_dnsmadeeasy', +) diff --git a/certbot/cli.py b/certbot/cli.py index 533acb4b2..562613b72 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1252,6 +1252,9 @@ def _plugins_parsing(helpful, plugins): helpful.add(["plugins", "certonly"], "--dns-dnsimple", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are ' 'using DNSimple for DNS).')) + helpful.add(["plugins", "certonly"], "--dns-dnsmadeeasy", action="store_true", + help=('Obtain certificates using a DNS TXT record (if you are' + 'using DNS Made Easy for DNS).')) helpful.add(["plugins", "certonly"], "--dns-google", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are ' 'using Google Cloud DNS).')) diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index 9a229d618..6f1bca25a 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -32,6 +32,7 @@ class PluginEntryPoint(object): "certbot-dns-cloudxns", "certbot-dns-digitalocean", "certbot-dns-dnsimple", + "certbot-dns-dnsmadeeasy", "certbot-dns-google", "certbot-dns-nsone", "certbot-dns-route53", diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 15008302d..0d216a0c3 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -134,8 +134,8 @@ def choose_plugin(prepared, question): return None noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", - "dns-digitalocean", "dns-dnsimple", "dns-google", "dns-route53", - "dns-nsone"] + "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-google", + "dns-route53", "dns-nsone"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -217,7 +217,7 @@ def set_configurator(previously, now): return now -def cli_plugin_requests(config): +def cli_plugin_requests(config): # pylint: disable=too-many-branches """ Figure out which plugins the user requested with CLI and config options @@ -247,6 +247,8 @@ def cli_plugin_requests(config): req_auth = set_configurator(req_auth, "dns-digitalocean") if config.dns_dnsimple: req_auth = set_configurator(req_auth, "dns-dnsimple") + if config.dns_dnsmadeeasy: + req_auth = set_configurator(req_auth, "dns-dnsmadeeasy") if config.dns_google: req_auth = set_configurator(req_auth, "dns-google") if config.dns_nsone: diff --git a/tools/venv.sh b/tools/venv.sh index 2d8e4f242..4ee08809c 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -18,6 +18,7 @@ fi -e certbot-dns-cloudxns \ -e certbot-dns-digitalocean \ -e certbot-dns-dnsimple \ + -e certbot-dns-dnsmadeeasy \ -e certbot-dns-google \ -e certbot-dns-nsone \ -e certbot-dns-route53 \ diff --git a/tools/venv3.sh b/tools/venv3.sh index 943507637..0df10c7ac 100755 --- a/tools/venv3.sh +++ b/tools/venv3.sh @@ -17,6 +17,7 @@ fi -e certbot-dns-cloudxns \ -e certbot-dns-digitalocean \ -e certbot-dns-dnsimple \ + -e certbot-dns-dnsmadeeasy \ -e certbot-dns-google \ -e certbot-dns-nsone \ -e certbot-dns-route53 \ diff --git a/tox.cover.sh b/tox.cover.sh index db8a6c500..b226f646a 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_google certbot_dns_nsone certbot_dns_route53 certbot_nginx letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_nsone certbot_dns_route53 certbot_nginx letshelp_certbot" else pkgs="$@" fi @@ -29,6 +29,8 @@ cover () { min=98 elif [ "$1" = "certbot_dns_dnsimple" ]; then min=98 + elif [ "$1" = "certbot_dns_dnsmadeeasy" ]; then + min=99 elif [ "$1" = "certbot_dns_google" ]; then min=99 elif [ "$1" = "certbot_dns_nsone" ]; then diff --git a/tox.ini b/tox.ini index 8c9aaaf26..ff7c44bb4 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,7 @@ py26_packages = non_py26_packages = certbot-dns-cloudxns \ certbot-dns-dnsimple \ + certbot-dns-dnsmadeeasy \ certbot-dns-nsone all_packages = {[base]py26_packages} {[base]non_py26_packages} @@ -45,6 +46,7 @@ source_paths = certbot-dns-cloudxns/certbot_dns_cloudxns certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-dnsimple/certbot_dns_dnsimple + certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy certbot-dns-google/certbot_dns_google certbot-dns-nsone/certbot_dns_nsone certbot-dns-route53/certbot_dns_route53 From f51d345d5b33901e54989175a06f2fda1464e636 Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Thu, 15 Jun 2017 17:14:38 -0700 Subject: [PATCH 057/125] Low-impact cleanup of IDisplay (#4818) Remove unused help-related display code. When NcursesDisplay was removed[1], help was deprecated. This change removes the remaining bits and pieces of code. Remove unused escape-related display code. When NcursesDisplay was removed[1], escape was deprecated. This change removes the remaining bits and pieces of code. Remove uses of unused menu parameters. Remove unused default_status/default_state argument from checklist. (This seems safe because not only is it unused, the parameter has different names in the interface and implementation) 1 - d54cb76432a2eff43cc9cc3c1cc4d9136eac2221 Resolves #4795. --- certbot-apache/certbot_apache/display_ops.py | 14 ++------------ .../certbot_apache/tests/display_ops_test.py | 3 --- certbot/cert_manager.py | 2 +- certbot/display/util.py | 11 +++++------ certbot/interfaces.py | 14 ++++++-------- certbot/main.py | 2 +- certbot/plugins/selection.py | 9 +-------- certbot/plugins/selection_test.py | 3 --- certbot/plugins/webroot.py | 18 ++---------------- certbot/plugins/webroot_test.py | 7 ++----- 10 files changed, 20 insertions(+), 63 deletions(-) diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index 6bcb64dd5..f9e0802f7 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -27,9 +27,7 @@ def select_vhost(domain, vhosts): return None while True: code, tag = _vhost_menu(domain, vhosts) - if code == display_util.HELP: - _more_info_vhost(vhosts[tag]) - elif code == display_util.OK: + if code == display_util.OK: return vhosts[tag] else: return None @@ -85,8 +83,7 @@ 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", force_interactive=True) + choices, 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 " @@ -96,10 +93,3 @@ def _vhost_menu(domain, vhosts): raise errors.MissingCommandlineFlag(msg) return code, tag - - -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)), - 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 f8b75022e..e59d411bd 100644 --- a/certbot-apache/certbot_apache/tests/display_ops_test.py +++ b/certbot-apache/certbot_apache/tests/display_ops_test.py @@ -43,13 +43,10 @@ class SelectVhostTest(unittest.TestCase): @certbot_util.patch_get_utility() def test_more_info_cancel(self, mock_util): mock_util().menu.side_effect = [ - (display_util.HELP, 1), - (display_util.HELP, 0), (display_util.CANCEL, -1), ] self.assertEqual(None, self._call(self.vhosts)) - self.assertEqual(mock_util().notification.call_count, 2) def test_no_vhosts(self): self.assertEqual(self._call([]), None) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 6519e7068..62cbfa695 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -157,7 +157,7 @@ def _get_certname(config, verb): 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", + choices, flag="--cert-name", force_interactive=True) if code != display_util.OK or not index in range(0, len(choices)): raise errors.Error("User ended interaction.") diff --git a/certbot/display/util.py b/certbot/display/util.py index f1922ebb9..194b46a24 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -24,10 +24,10 @@ CANCEL = "cancel" """Display exit code for a user canceling the display.""" HELP = "help" -"""Display exit code when for when the user requests more help.""" +"""Display exit code when for when the user requests more help. (UNUSED)""" ESC = "esc" -"""Display exit code when the user hits Escape""" +"""Display exit code when the user hits Escape (UNUSED)""" def _wrap_lines(msg): @@ -123,8 +123,8 @@ class FileDisplay(object): else: logger.debug("Not pausing for user confirmation") - def menu(self, message, choices, ok_label="", cancel_label="", - help_label="", default=None, + def menu(self, message, choices, ok_label=None, cancel_label=None, + help_label=None, default=None, cli_flag=None, force_interactive=False, **unused_kwargs): # pylint: disable=unused-argument """Display a menu. @@ -228,14 +228,13 @@ class FileDisplay(object): ans.startswith(no_label[0].upper())): return False - def checklist(self, message, tags, default_status=True, default=None, + def checklist(self, message, tags, 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 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 diff --git a/certbot/interfaces.py b/certbot/interfaces.py index f5c32f131..501a5c57e 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -396,8 +396,8 @@ class IDisplay(zope.interface.Interface): """ - def menu(message, choices, ok_label="OK", - cancel_label="Cancel", help_label="", + def menu(message, choices, ok_label=None, + cancel_label=None, help_label=None, default=None, cli_flag=None, force_interactive=False): """Displays a generic menu. @@ -409,9 +409,9 @@ class IDisplay(zope.interface.Interface): :param choices: choices :type choices: :class:`list` of :func:`tuple` or :class:`str` - :param str ok_label: label for OK button - :param str cancel_label: label for Cancel button - :param str help_label: label for Help button + :param str ok_label: label for OK button (UNUSED) + :param str cancel_label: label for Cancel button (UNUSED) + :param str help_label: label for Help button (UNUSED) :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 @@ -470,8 +470,7 @@ class IDisplay(zope.interface.Interface): """ - def checklist(message, tags, default_state, - default=None, cli_args=None, force_interactive=False): + def checklist(message, tags, 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 @@ -479,7 +478,6 @@ class IDisplay(zope.interface.Interface): :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 diff --git a/certbot/main.py b/certbot/main.py index d3f6eaa09..cd87706b4 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -161,7 +161,7 @@ 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", + response = display.menu(question, choices, default=0, force_interactive=True) if response[0] == display_util.CANCEL: # TODO: Add notification related to command-line options for diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 0d216a0c3..a8ebc47dd 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -112,7 +112,7 @@ def choose_plugin(prepared, question): while True: disp = z_util(interfaces.IDisplay) code, index = disp.menu( - question, opts, help_label="More Info", force_interactive=True) + question, opts, force_interactive=True) if code == display_util.OK: plugin_ep = prepared[index] @@ -123,13 +123,6 @@ def choose_plugin(prepared, question): "was:\n\n{0}".format(plugin_ep.prepare()), pause=False) else: return plugin_ep - elif code == display_util.HELP: - if prepared[index].misconfigured: - msg = "Reported Error: %s" % prepared[index].prepare() - else: - msg = prepared[index].init().more_info() - 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 41c2b55c9..9f0716905 100644 --- a/certbot/plugins/selection_test.py +++ b/certbot/plugins/selection_test.py @@ -137,13 +137,10 @@ class ChoosePluginTest(unittest.TestCase): @test_util.patch_get_utility("certbot.plugins.selection.z_util") def test_more_info(self, mock_util): mock_util().menu.side_effect = [ - (display_util.HELP, 0), - (display_util.HELP, 1), (display_util.OK, 1), ] self.assertEqual(self.mock_stand, self._call()) - self.assertEqual(mock_util().notification.call_count, 2) @test_util.patch_get_utility("certbot.plugins.selection.z_util") def test_no_choice(self, mock_util): diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index aad6ffc82..4662b2aa6 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -117,22 +117,11 @@ to serve all files under specified web root ({0}).""" code, index = display.menu( "Select the webroot for {0}:".format(domain), ["Enter a new webroot"] + known_webroots, - help_label="Help", cli_flag=path_flag, force_interactive=True) + cli_flag=path_flag, force_interactive=True) if code == display_util.CANCEL: raise errors.PluginError( "Every requested domain must have a " "webroot when using the webroot plugin.") - elif code == display_util.HELP: - display.notification( - "To use the webroot plugin, you need to have an " - "HTTP server running on this system serving files " - "for the requested domain. Additionally, this " - "server should be serving all files contained in a " - "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.", - force_interactive=True) else: # code == display_util.OK return None if index == 0 else known_webroots[index - 1] @@ -141,10 +130,7 @@ to serve all files under specified web root ({0}).""" _validate_webroot, "Input the webroot for {0}:".format(domain), force_interactive=True) - if code == display_util.HELP: - # Displaying help is not currently implemented - return None - elif code == display_util.CANCEL or code == display_util.ESC: + if code == display_util.CANCEL: return None else: # code == display_util.OK return _validate_webroot(webroot) diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py index 53809764b..e202a0a6d 100644 --- a/certbot/plugins/webroot_test.py +++ b/certbot/plugins/webroot_test.py @@ -84,10 +84,8 @@ class AuthenticatorTest(unittest.TestCase): self.config.webroot_map = {"otherthing.com": self.path} mock_display = mock_get_utility() - mock_display.menu.side_effect = ((display_util.HELP, -1), - (display_util.CANCEL, -1),) + mock_display.menu.side_effect = ((display_util.CANCEL, -1),) self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) - self.assertTrue(mock_display.notification.called) self.assertTrue(mock_display.menu.called) for call in mock_display.menu.call_args_list: self.assertTrue(self.achall.domain in call[0][0]) @@ -103,8 +101,7 @@ class AuthenticatorTest(unittest.TestCase): mock_display = mock_get_utility() mock_display.menu.return_value = (display_util.OK, 0,) with mock.patch('certbot.display.ops.validated_directory') as m: - m.side_effect = ((display_util.HELP, -1), - (display_util.CANCEL, -1), + m.side_effect = ((display_util.CANCEL, -1), (display_util.OK, self.path,)) self.auth.perform([self.achall]) From 87f6e18ac4b452e66c574a99da2b4ef626cfdd3b Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Fri, 16 Jun 2017 10:35:52 -0700 Subject: [PATCH 058/125] Add certbot-dns-dnsmadeeasy to release script (#4844) --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index ef15f1828..0fb15274d 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,7 +46,7 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-google certbot-dns-nsone certbot-dns-route53" +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-nsone certbot-dns-route53" # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" From 32f7e82a6929904218fb4fe433371940f8376cae Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Fri, 16 Jun 2017 13:30:24 -0700 Subject: [PATCH 059/125] add module-level documentation for DNS Made Easy (#4845) Add module-level documentation describing the use of certbot-dns-dnsmadeeasy, including discussion of credential management. --- .../certbot_dns_dnsmadeeasy/__init__.py | 87 ++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py index c8b78d3cf..52f055237 100644 --- a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py +++ b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/__init__.py @@ -1 +1,86 @@ -"""DNS Made Easy DNS Authenticator""" +""" +The `~certbot_dns_dnsmadeeasy.dns_dnsmadeeasy` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the DNS Made Easy API. + + +Named Arguments +--------------- + +========================================= ===================================== +``--dns-dnsmadeeasy-credentials`` DNS Made Easy credentials_ INI file. + (Required) +``--dns-dnsmadeeasy-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 60) +========================================= ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing DNS Made Easy API +credentials, obtained from your DNS Made Easy +`account page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # DNS Made Easy API credentials used by Certbot + dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a + dns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55 + +The path to this file can be provided interactively or using the +``--dns-dnsmadeeasy-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + DNS Made Easy account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-dnsmadeeasy \\ + --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-dnsmadeeasy \\ + --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 2 minutes + for DNS propagation + + certbot certonly \\ + --dns-dnsmadeeasy \\ + --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\ + --dns-dnsmadeeasy-propagation-seconds 120 \\ + -d example.com + +""" From 3f86e13acc9080717b62741d31961de15c80580d Mon Sep 17 00:00:00 2001 From: Zach Shepherd Date: Fri, 16 Jun 2017 14:43:12 -0700 Subject: [PATCH 060/125] LuaDNS DNS Authenticator (#4605) Implement an Authenticator which can fulfill a dns-01 challenge using the LuaDNS API. Applicable only for domains using LuaDNS for DNS. Testing Done: * `tox -e py27` * `tox -e lint` * Manual testing: * Used `certbot certonly --dns-luadns -d`, specifying a credentials file as a command line argument. Verified that a certificate was successfully obtained without user interaction. * Negative testing: * Path to non-existent credentials file. * Credentials file with unsafe permissions (644). * Path to credentials file without an email. * Path to credentials file with an invalid email. * Path to credentials file without a token. * Path to credentials file with an invalid token. * Domain name not registered to LuaDNS account. --- certbot-dns-luadns/LICENSE.txt | 190 ++++++++++++++++++ certbot-dns-luadns/MANIFEST.in | 3 + certbot-dns-luadns/README.rst | 1 + .../certbot_dns_luadns/__init__.py | 86 ++++++++ .../certbot_dns_luadns/dns_luadns.py | 83 ++++++++ .../certbot_dns_luadns/dns_luadns_test.py | 52 +++++ certbot-dns-luadns/docs/.gitignore | 1 + certbot-dns-luadns/docs/Makefile | 20 ++ certbot-dns-luadns/docs/api.rst | 8 + certbot-dns-luadns/docs/api/dns_luadns.rst | 5 + certbot-dns-luadns/docs/conf.py | 180 +++++++++++++++++ certbot-dns-luadns/docs/index.rst | 28 +++ certbot-dns-luadns/docs/make.bat | 36 ++++ certbot-dns-luadns/setup.cfg | 2 + certbot-dns-luadns/setup.py | 68 +++++++ certbot/cli.py | 3 + certbot/plugins/disco.py | 1 + certbot/plugins/selection.py | 5 +- tools/release.sh | 4 +- tools/venv.sh | 1 + tools/venv3.sh | 1 + tox.cover.sh | 4 +- tox.ini | 2 + 23 files changed, 780 insertions(+), 4 deletions(-) create mode 100644 certbot-dns-luadns/LICENSE.txt create mode 100644 certbot-dns-luadns/MANIFEST.in create mode 100644 certbot-dns-luadns/README.rst create mode 100644 certbot-dns-luadns/certbot_dns_luadns/__init__.py create mode 100644 certbot-dns-luadns/certbot_dns_luadns/dns_luadns.py create mode 100644 certbot-dns-luadns/certbot_dns_luadns/dns_luadns_test.py create mode 100644 certbot-dns-luadns/docs/.gitignore create mode 100644 certbot-dns-luadns/docs/Makefile create mode 100644 certbot-dns-luadns/docs/api.rst create mode 100644 certbot-dns-luadns/docs/api/dns_luadns.rst create mode 100644 certbot-dns-luadns/docs/conf.py create mode 100644 certbot-dns-luadns/docs/index.rst create mode 100644 certbot-dns-luadns/docs/make.bat create mode 100644 certbot-dns-luadns/setup.cfg create mode 100644 certbot-dns-luadns/setup.py diff --git a/certbot-dns-luadns/LICENSE.txt b/certbot-dns-luadns/LICENSE.txt new file mode 100644 index 000000000..981c46c9f --- /dev/null +++ b/certbot-dns-luadns/LICENSE.txt @@ -0,0 +1,190 @@ + 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/certbot-dns-luadns/MANIFEST.in b/certbot-dns-luadns/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-luadns/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-luadns/README.rst b/certbot-dns-luadns/README.rst new file mode 100644 index 000000000..e50843545 --- /dev/null +++ b/certbot-dns-luadns/README.rst @@ -0,0 +1 @@ +LuaDNS Authenticator plugin for Certbot diff --git a/certbot-dns-luadns/certbot_dns_luadns/__init__.py b/certbot-dns-luadns/certbot_dns_luadns/__init__.py new file mode 100644 index 000000000..e8e86f77c --- /dev/null +++ b/certbot-dns-luadns/certbot_dns_luadns/__init__.py @@ -0,0 +1,86 @@ +""" +The `~certbot_dns_luadns.dns_luadns` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the LuaDNS API. + + +Named Arguments +--------------- + +========================================= ===================================== +``--dns-luadns-credentials`` LuaDNS credentials_ INI file. + (Required) +``--dns-luadns-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +========================================= ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing LuaDNS API +credentials, obtained from your LuaDNS +`account settings page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # LuaDNS API credentials used by Certbot + dns_luadns_email = user@example.com + dns_luadns_token = 0123456789abcdef0123456789abcdef + +The path to this file can be provided interactively or using the +``--dns-luadns-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would the password to your + LuaDNS account. Users who can read this file can use these credentials + to issue arbitrary API calls on your behalf. Users who can cause Certbot to + run using these credentials can complete a ``dns-01`` challenge to acquire + new certificates or revoke existing certificates for associated domains, + even if those domains aren't being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-luadns \\ + --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-luadns \\ + --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 2 minutes + for DNS propagation + + certbot certonly \\ + --dns-luadns \\ + --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\ + --dns-luadns-propagation-seconds 120 \\ + -d example.com + +""" diff --git a/certbot-dns-luadns/certbot_dns_luadns/dns_luadns.py b/certbot-dns-luadns/certbot_dns_luadns/dns_luadns.py new file mode 100644 index 000000000..00b62e6e1 --- /dev/null +++ b/certbot-dns-luadns/certbot_dns_luadns/dns_luadns.py @@ -0,0 +1,83 @@ +"""DNS Authenticator for LuaDNS DNS.""" +import logging + +import zope.interface +from lexicon.providers import luadns + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common +from certbot.plugins import dns_common_lexicon + +logger = logging.getLogger(__name__) + +ACCOUNT_URL = 'https://api.luadns.com/settings' + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for LuaDNS + + This Authenticator uses the LuaDNS API to fulfill a dns-01 challenge. + """ + + description = 'Obtain certificates using a DNS TXT record (if you are using LuaDNS for DNS).' + ttl = 60 + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.credentials = None + + @classmethod + def add_parser_arguments(cls, add): # pylint: disable=arguments-differ + super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=30) + add('credentials', help='LuaDNS credentials INI file.') + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ + 'the LuaDNS API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'LuaDNS credentials INI file', + { + 'email': 'email address associated with LuaDNS account', + 'token': 'API token for LuaDNS account, obtained from {0}'.format(ACCOUNT_URL) + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_luadns_client().add_txt_record(domain, validation_name, validation) + + def _cleanup(self, domain, validation_name, validation): + self._get_luadns_client().del_txt_record(domain, validation_name, validation) + + def _get_luadns_client(self): + return _LuaDNSLexiconClient(self.credentials.conf('email'), + self.credentials.conf('token'), + self.ttl) + + +class _LuaDNSLexiconClient(dns_common_lexicon.LexiconClient): + """ + Encapsulates all communication with the LuaDNS via Lexicon. + """ + + def __init__(self, email, token, ttl): + super(_LuaDNSLexiconClient, self).__init__() + + self.provider = luadns.Provider({ + 'auth_username': email, + 'auth_token': token, + 'ttl': ttl, + }) + + def _handle_http_error(self, e, domain_name): + hint = None + if str(e).startswith('401 Client Error: Unauthorized for url:'): + hint = 'Are your email and API token values correct?' + + return errors.PluginError('Error determining zone identifier for {0}: {1}.{2}' + .format(domain_name, e, ' ({0})'.format(hint) if hint else '')) diff --git a/certbot-dns-luadns/certbot_dns_luadns/dns_luadns_test.py b/certbot-dns-luadns/certbot_dns_luadns/dns_luadns_test.py new file mode 100644 index 000000000..bf77e03e4 --- /dev/null +++ b/certbot-dns-luadns/certbot_dns_luadns/dns_luadns_test.py @@ -0,0 +1,52 @@ +"""Tests for certbot_dns_luadns.dns_luadns.""" + +import os +import unittest + +import mock +from requests.exceptions import HTTPError + +from certbot.plugins import dns_test_common +from certbot.plugins import dns_test_common_lexicon +from certbot.tests import util as test_util + +EMAIL = 'fake@example.com' +TOKEN = 'foo' + + +class AuthenticatorTest(test_util.TempDirTestCase, + dns_test_common_lexicon.BaseLexiconAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_luadns.dns_luadns import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write({"luadns_email": EMAIL, "luadns_token": TOKEN}, path) + + self.config = mock.MagicMock(luadns_credentials=path, + luadns_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "luadns") + + self.mock_client = mock.MagicMock() + # _get_luadns_client | pylint: disable=protected-access + self.auth._get_luadns_client = mock.MagicMock(return_value=self.mock_client) + + +class LuaDNSLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + + LOGIN_ERROR = HTTPError("401 Client Error: Unauthorized for url: ...") + + def setUp(self): + from certbot_dns_luadns.dns_luadns import _LuaDNSLexiconClient + + self.client = _LuaDNSLexiconClient(EMAIL, TOKEN, 0) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-luadns/docs/.gitignore b/certbot-dns-luadns/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-luadns/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-luadns/docs/Makefile b/certbot-dns-luadns/docs/Makefile new file mode 100644 index 000000000..2f65aeccc --- /dev/null +++ b/certbot-dns-luadns/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = certbot-dns-luadns +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/certbot-dns-luadns/docs/api.rst b/certbot-dns-luadns/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-luadns/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-luadns/docs/api/dns_luadns.rst b/certbot-dns-luadns/docs/api/dns_luadns.rst new file mode 100644 index 000000000..9aecbaf05 --- /dev/null +++ b/certbot-dns-luadns/docs/api/dns_luadns.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_luadns.dns_luadns` +---------------------------------- + +.. automodule:: certbot_dns_luadns.dns_luadns + :members: diff --git a/certbot-dns-luadns/docs/conf.py b/certbot-dns-luadns/docs/conf.py new file mode 100644 index 000000000..bd81d5a5f --- /dev/null +++ b/certbot-dns-luadns/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-luadns documentation build configuration file, created by +# sphinx-quickstart on Wed May 10 18:46:01 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-luadns' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-luadnsdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-luadns.tex', u'certbot-dns-luadns Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-luadns', u'certbot-dns-luadns Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-luadns', u'certbot-dns-luadns Documentation', + author, 'certbot-dns-luadns', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-luadns/docs/index.rst b/certbot-dns-luadns/docs/index.rst new file mode 100644 index 000000000..589e925c0 --- /dev/null +++ b/certbot-dns-luadns/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-luadns documentation master file, created by + sphinx-quickstart on Wed May 10 18:46:01 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-luadns's documentation! +============================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_luadns + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-luadns/docs/make.bat b/certbot-dns-luadns/docs/make.bat new file mode 100644 index 000000000..9cfe0400e --- /dev/null +++ b/certbot-dns-luadns/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-luadns + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-luadns/setup.cfg b/certbot-dns-luadns/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-luadns/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py new file mode 100644 index 000000000..c964b01da --- /dev/null +++ b/certbot-dns-luadns/setup.py @@ -0,0 +1,68 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.16.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme=={0}'.format(version), + 'certbot=={0}'.format(version), + 'dns-lexicon', + 'mock', + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-luadns', + version=version, + description="LuaDNS Authenticator plugin for Certbot", + url='https://github.com/certbot/certbot', + 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.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + '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, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-luadns = certbot_dns_luadns.dns_luadns:Authenticator', + ], + }, + test_suite='certbot_dns_luadns', +) diff --git a/certbot/cli.py b/certbot/cli.py index 562613b72..efd7fba6f 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1258,6 +1258,9 @@ def _plugins_parsing(helpful, plugins): helpful.add(["plugins", "certonly"], "--dns-google", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are ' 'using Google Cloud DNS).')) + helpful.add(["plugins", "certonly"], "--dns-luadns", action="store_true", + help=('Obtain certificates using a DNS TXT record (if you are ' + 'using LuaDNS for DNS).')) helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are ' 'using NS1 for DNS).')) diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index 6f1bca25a..defabf75d 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -34,6 +34,7 @@ class PluginEntryPoint(object): "certbot-dns-dnsimple", "certbot-dns-dnsmadeeasy", "certbot-dns-google", + "certbot-dns-luadns", "certbot-dns-nsone", "certbot-dns-route53", "certbot-nginx", diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index a8ebc47dd..52fcb9e11 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -128,7 +128,7 @@ def choose_plugin(prepared, question): noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-google", - "dns-route53", "dns-nsone"] + "dns-luadns", "dns-route53", "dns-nsone"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -220,6 +220,7 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches req_inst = req_auth = config.configurator req_inst = set_configurator(req_inst, config.installer) req_auth = set_configurator(req_auth, config.authenticator) + if config.nginx: req_inst = set_configurator(req_inst, "nginx") req_auth = set_configurator(req_auth, "nginx") @@ -244,6 +245,8 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches req_auth = set_configurator(req_auth, "dns-dnsmadeeasy") if config.dns_google: req_auth = set_configurator(req_auth, "dns-google") + if config.dns_luadns: + req_auth = set_configurator(req_auth, "dns-luadns") if config.dns_nsone: req_auth = set_configurator(req_auth, "dns-nsone") if config.dns_route53: diff --git a/tools/release.sh b/tools/release.sh index 0fb15274d..2ae0e630e 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,8 +46,8 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-nsone certbot-dns-route53" - +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-luadns certbot-dns-nsone certbot-dns-route53" +x # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" SUBPKGS_NO_CERTBOT="$SUBPKGS_IN_AUTO_NO_CERTBOT $SUBPKGS_NOT_IN_AUTO" diff --git a/tools/venv.sh b/tools/venv.sh index 4ee08809c..531057ec4 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -20,6 +20,7 @@ fi -e certbot-dns-dnsimple \ -e certbot-dns-dnsmadeeasy \ -e certbot-dns-google \ + -e certbot-dns-luadns \ -e certbot-dns-nsone \ -e certbot-dns-route53 \ -e certbot-nginx \ diff --git a/tools/venv3.sh b/tools/venv3.sh index 0df10c7ac..da56c2249 100755 --- a/tools/venv3.sh +++ b/tools/venv3.sh @@ -19,6 +19,7 @@ fi -e certbot-dns-dnsimple \ -e certbot-dns-dnsmadeeasy \ -e certbot-dns-google \ + -e certbot-dns-luadns \ -e certbot-dns-nsone \ -e certbot-dns-route53 \ -e certbot-nginx \ diff --git a/tox.cover.sh b/tox.cover.sh index b226f646a..76fc9f62c 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_nsone certbot_dns_route53 certbot_nginx letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_route53 certbot_nginx letshelp_certbot" else pkgs="$@" fi @@ -33,6 +33,8 @@ cover () { min=99 elif [ "$1" = "certbot_dns_google" ]; then min=99 + elif [ "$1" = "certbot_dns_luadns" ]; then + min=98 elif [ "$1" = "certbot_dns_nsone" ]; then min=99 elif [ "$1" = "certbot_dns_route53" ]; then diff --git a/tox.ini b/tox.ini index ff7c44bb4..87962d6c4 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,7 @@ non_py26_packages = certbot-dns-cloudxns \ certbot-dns-dnsimple \ certbot-dns-dnsmadeeasy \ + certbot-dns-luadns \ certbot-dns-nsone all_packages = {[base]py26_packages} {[base]non_py26_packages} @@ -48,6 +49,7 @@ source_paths = certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy certbot-dns-google/certbot_dns_google + certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone certbot-dns-route53/certbot_dns_route53 certbot-nginx/certbot_nginx From bb8e504a02a4eef63f193851fa093980875adc35 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 16 Jun 2017 15:01:13 -0700 Subject: [PATCH 061/125] Add warning about changing the UA (#4843) --- certbot/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certbot/client.py b/certbot/client.py index af2868f6a..7896ab7dc 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -53,6 +53,9 @@ def determine_user_agent(config): :rtype: `str` """ + # WARNING: To ensure changes are in line with Certbot's privacy + # policy, talk to a core Certbot team member before making any + # changes here. if config.user_agent is None: ua = ("CertbotACMEClient/{0} ({1}; {2}) Authenticator/{3} Installer/{4} " "({5}; flags: {6}) Py/{7}") From 811d436d5a8a12101e6bda81ef2ff37f185af12f Mon Sep 17 00:00:00 2001 From: Matt Dainty Date: Sat, 17 Jun 2017 00:53:46 +0100 Subject: [PATCH 062/125] RFC 2136 DNS Authenticator (#4701) Introduce a plugin that automates the process of completing a dns-01 challenge by creating, and subsequently removing, TXT records using RFC 2136 Dynamic Updates (a.k.a. nsupdate). This plugin has been tested with BIND, but may work with other RFC 2136-compatible DNS servers, such as PowerDNS. --- certbot-dns-rfc2136/LICENSE.txt | 190 +++++++++++++++ certbot-dns-rfc2136/MANIFEST.in | 3 + certbot-dns-rfc2136/README.rst | 1 + .../certbot_dns_rfc2136/__init__.py | 136 +++++++++++ .../certbot_dns_rfc2136/dns_rfc2136.py | 221 ++++++++++++++++++ .../certbot_dns_rfc2136/dns_rfc2136_test.py | 197 ++++++++++++++++ certbot-dns-rfc2136/docs/.gitignore | 1 + certbot-dns-rfc2136/docs/Makefile | 20 ++ certbot-dns-rfc2136/docs/api.rst | 8 + certbot-dns-rfc2136/docs/api/dns_rfc2136.rst | 5 + certbot-dns-rfc2136/docs/conf.py | 180 ++++++++++++++ certbot-dns-rfc2136/docs/index.rst | 28 +++ certbot-dns-rfc2136/docs/make.bat | 36 +++ certbot-dns-rfc2136/setup.cfg | 2 + certbot-dns-rfc2136/setup.py | 69 ++++++ certbot/cli.py | 2 + certbot/plugins/disco.py | 1 + certbot/plugins/dns_common.py | 16 +- certbot/plugins/selection.py | 4 +- tools/release.sh | 4 +- tools/venv.sh | 1 + tox.cover.sh | 4 +- tox.ini | 2 + 23 files changed, 1125 insertions(+), 6 deletions(-) create mode 100644 certbot-dns-rfc2136/LICENSE.txt create mode 100644 certbot-dns-rfc2136/MANIFEST.in create mode 100644 certbot-dns-rfc2136/README.rst create mode 100644 certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py create mode 100644 certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py create mode 100644 certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py create mode 100644 certbot-dns-rfc2136/docs/.gitignore create mode 100644 certbot-dns-rfc2136/docs/Makefile create mode 100644 certbot-dns-rfc2136/docs/api.rst create mode 100644 certbot-dns-rfc2136/docs/api/dns_rfc2136.rst create mode 100644 certbot-dns-rfc2136/docs/conf.py create mode 100644 certbot-dns-rfc2136/docs/index.rst create mode 100644 certbot-dns-rfc2136/docs/make.bat create mode 100644 certbot-dns-rfc2136/setup.cfg create mode 100644 certbot-dns-rfc2136/setup.py diff --git a/certbot-dns-rfc2136/LICENSE.txt b/certbot-dns-rfc2136/LICENSE.txt new file mode 100644 index 000000000..981c46c9f --- /dev/null +++ b/certbot-dns-rfc2136/LICENSE.txt @@ -0,0 +1,190 @@ + 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/certbot-dns-rfc2136/MANIFEST.in b/certbot-dns-rfc2136/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-rfc2136/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-rfc2136/README.rst b/certbot-dns-rfc2136/README.rst new file mode 100644 index 000000000..e8ac2b337 --- /dev/null +++ b/certbot-dns-rfc2136/README.rst @@ -0,0 +1 @@ +RFC 2136 DNS Authenticator plugin for Certbot diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py new file mode 100644 index 000000000..0f97869e2 --- /dev/null +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py @@ -0,0 +1,136 @@ +""" +The `~certbot_dns_rfc2136.dns_rfc2136` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using RFC 2136 Dynamic Updates. + + +Named Arguments +--------------- + +===================================== ===================================== +``--dns-rfc2136-credentials`` RFC 2136 credentials_ INI file. + (Required) +``--dns-rfc2136-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 60) +===================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing the target DNS +server that supports RFC 2136 Dynamic Updates, the name of the TSIG key, the +TSIG key secret itself and the algorithm used if it's different to HMAC-MD5. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # Target DNS server + dns_rfc2136_server = 192.0.2.1 + # TSIG key name + dns_rfc2136_name = keyname. + # TSIG key secret + dns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \ +AmKd7ak51vWKgSl12ib86oQRPkpDjg== + # TSIG key algorithm + dns_rfc2136_algorithm = HMAC-SHA512 + +The path to this file can be provided interactively or using the +``--dns-rfc2136-credentials`` command-line argument. Certbot records the +path to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect this TSIG key material as it can be used to potentially + add, update, or delete any record in the target DNS server. Users who can + read this file can use these credentials to issue arbitrary API calls on + your behalf. Users who can cause Certbot to run using these credentials can + complete a ``dns-01`` challenge to acquire new certificates or revoke + existing certificates for associated domains, even if those domains aren't + being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + +Sample BIND configuration +''''''''''''''''''''''''' + +Here's a sample BIND configuration for Certbot to use. You will need to +generate a new TSIG key, include it in the BIND configuration and grant it +permission to issue updates on the target DNS zone. + +.. code-block:: bash + :caption: Generate a new SHA512 TSIG key + + dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST keyname. + +.. note:: + There are a few tools shipped with BIND that can all generate TSIG keys; + ``dnssec-keygen``, ``rndc-confgen``, and ``ddns-confgen``. Try and use the + most secure algorithm supported by your DNS server. + +.. code-block:: none + :caption: Sample BIND configuration + + key "keyname." { + algorithm hmac-sha512; + secret "4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \ +AmKd7ak51vWKgSl12ib86oQRPkpDjg=="; + }; + + zone "example.com." IN { + type master; + file "named.example.com"; + update-policy { + grant keyname. name _acme-challenge.example.com. txt; + }; + }; + +.. note:: + This configuration limits the scope of the TSIG key to just be able to + add and remove TXT records for one specific host for the purpose of + completing the ``dns-01`` challenge. If your version of BIND doesn't + support the + `update-policy `_ + directive then you can use the less-secure + `allow-update `_ + directive instead. + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-rfc2136 \\ + --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-rfc2136 \\ + --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 30 seconds + for DNS propagation + + certbot certonly \\ + --dns-rfc2136 \\ + --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\ + --dns-rfc2136-propagation-seconds 30 \\ + -d example.com + +""" diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py new file mode 100644 index 000000000..b4b71bb75 --- /dev/null +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py @@ -0,0 +1,221 @@ +"""DNS Authenticator using RFC 2136 Dynamic Updates.""" +import logging + +import dns.flags +import dns.message +import dns.name +import dns.query +import dns.rdataclass +import dns.rdatatype +import dns.tsig +import dns.tsigkeyring +import dns.update +import zope.interface + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common + +logger = logging.getLogger(__name__) + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator using RFC 2136 Dynamic Updates + + This Authenticator uses RFC 2136 Dynamic Updates to fulfull a dns-01 challenge. + """ + + ALGORITHMS = { + 'HMAC-MD5': dns.tsig.HMAC_MD5, + 'HMAC-SHA1': dns.tsig.HMAC_SHA1, + 'HMAC-SHA224': dns.tsig.HMAC_SHA224, + 'HMAC-SHA256': dns.tsig.HMAC_SHA256, + 'HMAC-SHA384': dns.tsig.HMAC_SHA384, + 'HMAC-SHA512': dns.tsig.HMAC_SHA512 + } + + description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).' + ttl = 120 + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.credentials = None + + @classmethod + def add_parser_arguments(cls, add): # pylint: disable=arguments-differ + super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60) + add('credentials', help='RFC 2136 credentials INI file.') + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ + 'RFC 2136 Dynamic Updates.' + + def _validate_algorithm(self, credentials): + algorithm = credentials.conf('algorithm') + if algorithm: + if not self.ALGORITHMS.get(algorithm): + raise errors.PluginError("Unknown algorithm: {0}.".format(algorithm)) + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'RFC 2136 credentials INI file', + { + 'name': 'TSIG key name', + 'secret': 'TSIG key secret', + 'server': 'The target DNS server' + }, + self._validate_algorithm + ) + + def _perform(self, domain, validation_name, validation): + self._get_rfc2136_client().add_txt_record(domain, validation_name, validation, self.ttl) + + def _cleanup(self, domain, validation_name, validation): + self._get_rfc2136_client().del_txt_record(domain, validation_name, validation) + + def _get_rfc2136_client(self): + return _RFC2136Client(self.credentials.conf('server'), + self.credentials.conf('name'), + self.credentials.conf('secret'), + self.ALGORITHMS.get(self.credentials.conf('algorithm'), + dns.tsig.HMAC_MD5)) + + +class _RFC2136Client(object): + """ + Encapsulates all communication with the target DNS server. + """ + def __init__(self, server, key_name, key_secret, key_algorithm): + self.server = server + self.keyring = dns.tsigkeyring.from_text({ + key_name: key_secret + }) + self.algorithm = key_algorithm + + def add_txt_record(self, domain_name, record_name, record_content, record_ttl): + """ + Add a TXT record using the supplied information. + + :param str domain: The domain to use to find the closest SOA. + :param str record_name: The record name (typically beginning with '_acme-challenge.'). + :param str record_content: The record content (typically the challenge validation). + :param int record_ttl: The record TTL (number of seconds that the record may be cached). + :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server + """ + + domain = self._find_domain(domain_name) + + n = dns.name.from_text(record_name) + o = dns.name.from_text(domain) + rel = n.relativize(o) + + update = dns.update.Update( + domain, + keyring=self.keyring, + keyalgorithm=self.algorithm) + update.add(rel, record_ttl, dns.rdatatype.TXT, record_content) + + try: + response = dns.query.tcp(update, self.server) + except Exception as e: + raise errors.PluginError('Encountered error adding TXT record: {0}' + .format(e)) + rcode = response.rcode() + + if rcode == dns.rcode.NOERROR: + logger.debug('Successfully added TXT record') + else: + raise errors.PluginError('Received response from server: {0}' + .format(dns.rcode.to_text(rcode))) + + def del_txt_record(self, domain_name, record_name, record_content): + """ + Delete a TXT record using the supplied information. + + :param str domain: The domain to use to find the closest SOA. + :param str record_name: The record name (typically beginning with '_acme-challenge.'). + :param str record_content: The record content (typically the challenge validation). + :param int record_ttl: The record TTL (number of seconds that the record may be cached). + :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server + """ + + domain = self._find_domain(domain_name) + + n = dns.name.from_text(record_name) + o = dns.name.from_text(domain) + rel = n.relativize(o) + + update = dns.update.Update( + domain, + keyring=self.keyring, + keyalgorithm=self.algorithm) + update.delete(rel, dns.rdatatype.TXT, record_content) + + try: + response = dns.query.tcp(update, self.server) + except Exception as e: + raise errors.PluginError('Encountered error deleting TXT record: {0}' + .format(e)) + rcode = response.rcode() + + if rcode == dns.rcode.NOERROR: + logger.debug('Successfully deleted TXT record') + else: + raise errors.PluginError('Received response from server: {0}' + .format(dns.rcode.to_text(rcode))) + + def _find_domain(self, domain_name): + """ + Find the closest domain with an SOA record for a given domain name. + + :param str domain_name: The domain name for which to find the closest SOA record. + :returns: The domain, if found. + :rtype: str + :raises certbot.errors.PluginError: if no SOA record can be found. + """ + + domain_name_guesses = dns_common.base_domain_name_guesses(domain_name) + + # Loop through until we find an authoritative SOA record + for guess in domain_name_guesses: + if self._query_soa(guess): + return guess + + raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.' + .format(domain_name, domain_name_guesses)) + + def _query_soa(self, domain_name): + """ + Query a domain name for an authoritative SOA record. + + :param str domain_name: The domain name to query for an SOA record. + :returns: True if found, False otherwise. + :rtype: bool + :raises certbot.errors.PluginError: if no response is received. + """ + + domain = dns.name.from_text(domain_name) + + request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN) + # Turn off Recursion Desired bit in query + request.flags ^= dns.flags.RD + + try: + response = dns.query.udp(request, self.server) + rcode = response.rcode() + + # Authoritative Answer bit should be set + if (rcode == dns.rcode.NOERROR and len(response.answer) > 0 and + response.flags & dns.flags.AA): + logger.debug('Received authoritative SOA response for %s', domain_name) + return True + + logger.debug('No authoritative SOA record found for %s', domain_name) + return False + except Exception as e: + raise errors.PluginError('Encountered error when making query: {0}' + .format(e)) + diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py new file mode 100644 index 000000000..54fdb8575 --- /dev/null +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py @@ -0,0 +1,197 @@ +"""Tests for certbot_dns_rfc2136.dns_rfc2136.""" + +import os +import unittest + +import dns.flags +import dns.rcode +import dns.tsig +import mock + +from certbot import errors +from certbot.plugins import dns_test_common +from certbot.plugins.dns_test_common import DOMAIN +from certbot.tests import util as test_util + +SERVER = '192.0.2.1' +NAME = 'a-tsig-key.' +SECRET = 'SSB3b25kZXIgd2hvIHdpbGwgYm90aGVyIHRvIGRlY29kZSB0aGlzIHRleHQK' +VALID_CONFIG = {"rfc2136_server": SERVER, "rfc2136_name": NAME, "rfc2136_secret": SECRET} + + +class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): + + def setUp(self): + from certbot_dns_rfc2136.dns_rfc2136 import Authenticator + + super(AuthenticatorTest, self).setUp() + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write(VALID_CONFIG, path) + + self.config = mock.MagicMock(rfc2136_credentials=path, + rfc2136_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "rfc2136") + + self.mock_client = mock.MagicMock() + # _get_rfc2136_client | pylint: disable=protected-access + self.auth._get_rfc2136_client = mock.MagicMock(return_value=self.mock_client) + + def test_perform(self): + self.auth.perform([self.achall]) + + expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] + self.assertEqual(expected, self.mock_client.mock_calls) + + def test_cleanup(self): + # _attempt_cleanup | pylint: disable=protected-access + self.auth._attempt_cleanup = True + self.auth.cleanup([self.achall]) + + expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] + self.assertEqual(expected, self.mock_client.mock_calls) + + def test_invalid_algorithm_raises(self): + config = VALID_CONFIG.copy() + config["rfc2136_algorithm"] = "INVALID" + dns_test_common.write(config, self.config.rfc2136_credentials) + + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + def test_valid_algorithm_passes(self): + config = VALID_CONFIG.copy() + config["rfc2136_algorithm"] = "HMAC-SHA512" + dns_test_common.write(config, self.config.rfc2136_credentials) + + self.auth.perform([self.achall]) + + +class RFC2136ClientTest(unittest.TestCase): + + def setUp(self): + from certbot_dns_rfc2136.dns_rfc2136 import _RFC2136Client + + self.rfc2136_client = _RFC2136Client(SERVER, NAME, SECRET, dns.tsig.HMAC_MD5) + + @mock.patch("dns.query.tcp") + def test_add_txt_record(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NOERROR + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.rfc2136_client.add_txt_record(DOMAIN, "bar", "baz", 42) + + query_mock.assert_called_with(mock.ANY, SERVER) + self.assertTrue("bar. 42 IN TXT \"baz\"" in str(query_mock.call_args[0][0])) + + @mock.patch("dns.query.tcp") + def test_add_txt_record_wraps_errors(self, query_mock): + query_mock.side_effect = Exception + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.assertRaises( + errors.PluginError, + self.rfc2136_client.add_txt_record, + DOMAIN, "bar", "baz", 42) + + @mock.patch("dns.query.tcp") + def test_add_txt_record_server_error(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.assertRaises( + errors.PluginError, + self.rfc2136_client.add_txt_record, + DOMAIN, "bar", "baz", 42) + + @mock.patch("dns.query.tcp") + def test_del_txt_record(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NOERROR + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.rfc2136_client.del_txt_record(DOMAIN, "bar", "baz") + + query_mock.assert_called_with(mock.ANY, SERVER) + self.assertTrue("bar. 0 NONE TXT \"baz\"" in str(query_mock.call_args[0][0])) + + @mock.patch("dns.query.tcp") + def test_del_txt_record_wraps_errors(self, query_mock): + query_mock.side_effect = Exception + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.assertRaises( + errors.PluginError, + self.rfc2136_client.del_txt_record, + DOMAIN, "bar", "baz") + + @mock.patch("dns.query.tcp") + def test_del_txt_record_server_error(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") + + self.assertRaises( + errors.PluginError, + self.rfc2136_client.del_txt_record, + DOMAIN, "bar", "baz") + + def test_find_domain(self): + # _query_soa | pylint: disable=protected-access + self.rfc2136_client._query_soa = mock.MagicMock(side_effect=[False, False, True]) + + # _find_domain | pylint: disable=protected-access + domain = self.rfc2136_client._find_domain('foo.bar.'+DOMAIN) + + self.assertTrue(domain == DOMAIN) + + def test_find_domain_wraps_errors(self): + # _query_soa | pylint: disable=protected-access + self.rfc2136_client._query_soa = mock.MagicMock(return_value=False) + + self.assertRaises( + errors.PluginError, + # _find_domain | pylint: disable=protected-access + self.rfc2136_client._find_domain, + 'foo.bar.'+DOMAIN) + + @mock.patch("dns.query.udp") + def test_query_soa_found(self, query_mock): + query_mock.return_value = mock.MagicMock(answer=[mock.MagicMock()], flags=dns.flags.AA) + query_mock.return_value.rcode.return_value = dns.rcode.NOERROR + + # _query_soa | pylint: disable=protected-access + result = self.rfc2136_client._query_soa(DOMAIN) + + query_mock.assert_called_with(mock.ANY, SERVER) + self.assertTrue(result == True) + + @mock.patch("dns.query.udp") + def test_query_soa_not_found(self, query_mock): + query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN + + # _query_soa | pylint: disable=protected-access + result = self.rfc2136_client._query_soa(DOMAIN) + + query_mock.assert_called_with(mock.ANY, SERVER) + self.assertTrue(result == False) + + @mock.patch("dns.query.udp") + def test_query_soa_wraps_errors(self, query_mock): + query_mock.side_effect = Exception + + self.assertRaises( + errors.PluginError, + # _query_soa | pylint: disable=protected-access + self.rfc2136_client._query_soa, + DOMAIN) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-rfc2136/docs/.gitignore b/certbot-dns-rfc2136/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-rfc2136/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-rfc2136/docs/Makefile b/certbot-dns-rfc2136/docs/Makefile new file mode 100644 index 000000000..c7aea1792 --- /dev/null +++ b/certbot-dns-rfc2136/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = certbot-dns-rfc2136 +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/certbot-dns-rfc2136/docs/api.rst b/certbot-dns-rfc2136/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-rfc2136/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-rfc2136/docs/api/dns_rfc2136.rst b/certbot-dns-rfc2136/docs/api/dns_rfc2136.rst new file mode 100644 index 000000000..f5e98454a --- /dev/null +++ b/certbot-dns-rfc2136/docs/api/dns_rfc2136.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_rfc2136.dns_rfc2136` +-------------------------------------- + +.. automodule:: certbot_dns_rfc2136.dns_rfc2136 + :members: diff --git a/certbot-dns-rfc2136/docs/conf.py b/certbot-dns-rfc2136/docs/conf.py new file mode 100644 index 000000000..8cc5d595f --- /dev/null +++ b/certbot-dns-rfc2136/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-rfc2136 documentation build configuration file, created by +# sphinx-quickstart on Thu Jun 15 06:42:51 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-rfc2136' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-rfc2136doc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-rfc2136.tex', u'certbot-dns-rfc2136 Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-rfc2136', u'certbot-dns-rfc2136 Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-rfc2136', u'certbot-dns-rfc2136 Documentation', + author, 'certbot-dns-rfc2136', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-rfc2136/docs/index.rst b/certbot-dns-rfc2136/docs/index.rst new file mode 100644 index 000000000..71705cb7f --- /dev/null +++ b/certbot-dns-rfc2136/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-rfc2136 documentation master file, created by + sphinx-quickstart on Thu Jun 15 06:42:51 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-rfc2136's documentation! +=============================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_rfc2136 + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-rfc2136/docs/make.bat b/certbot-dns-rfc2136/docs/make.bat new file mode 100644 index 000000000..8d09ca8fd --- /dev/null +++ b/certbot-dns-rfc2136/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-rfc2136 + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-rfc2136/setup.cfg b/certbot-dns-rfc2136/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-rfc2136/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py new file mode 100644 index 000000000..e1d54e4bd --- /dev/null +++ b/certbot-dns-rfc2136/setup.py @@ -0,0 +1,69 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.16.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme=={0}'.format(version), + 'certbot=={0}'.format(version), + 'dnspython', + 'mock', + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-rfc2136', + version=version, + description="RFC 2136 DNS Authenticator plugin for Certbot", + url='https://github.com/certbot/certbot', + 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', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + '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, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-rfc2136 = certbot_dns_rfc2136.dns_rfc2136:Authenticator', + ], + }, + test_suite='certbot_dns_rfc2136', +) diff --git a/certbot/cli.py b/certbot/cli.py index efd7fba6f..14874e63e 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1264,6 +1264,8 @@ def _plugins_parsing(helpful, plugins): helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are ' 'using NS1 for DNS).')) + helpful.add(["plugins", "certonly"], "--dns-rfc2136", action="store_true", + help='Obtain certificates using a DNS TXT record (if you are using BIND for DNS).') helpful.add(["plugins", "certonly"], "--dns-route53", action="store_true", help=('Obtain certificates using a DNS TXT record (if you are using Route53 for ' 'DNS).')) diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index defabf75d..37baf98f7 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -36,6 +36,7 @@ class PluginEntryPoint(object): "certbot-dns-google", "certbot-dns-luadns", "certbot-dns-nsone", + "certbot-dns-rfc2136", "certbot-dns-route53", "certbot-nginx", ] diff --git a/certbot/plugins/dns_common.py b/certbot/plugins/dns_common.py index f71905d79..ba88b7aef 100644 --- a/certbot/plugins/dns_common.py +++ b/certbot/plugins/dns_common.py @@ -139,7 +139,7 @@ class DNSAuthenticator(common.Plugin): setattr(self.config, self.dest(key), os.path.abspath(os.path.expanduser(new_value))) - def _configure_credentials(self, key, label, required_variables=None): + def _configure_credentials(self, key, label, required_variables=None, validator=None): """ As `_configure_file`, but for a credential configuration file. @@ -150,11 +150,20 @@ class DNSAuthenticator(common.Plugin): :param str key: The configuration key. :param str label: The user-friendly label for this piece of information. :param dict required_variables: Map of variable which must be present to error to display. + :param callable validator: A method which will be called to validate the + `CredentialsConfiguration` resulting from the supplied input after it has been validated + to contain the `required_variables`. Should throw a `~certbot.errors.PluginError` to + indicate any issue. """ def __validator(filename): + configuration = CredentialsConfiguration(filename, self.dest) + if required_variables: - CredentialsConfiguration(filename, self.dest).require(required_variables) + configuration.require(required_variables) + + if validator: + validator(configuration) self._configure_file(key, label, __validator) @@ -162,6 +171,9 @@ class DNSAuthenticator(common.Plugin): if required_variables: credentials_configuration.require(required_variables) + if validator: + validator(credentials_configuration) + return credentials_configuration @staticmethod diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 52fcb9e11..fdfbf2b15 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -128,7 +128,7 @@ def choose_plugin(prepared, question): noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-google", - "dns-luadns", "dns-route53", "dns-nsone"] + "dns-luadns", "dns-nsone", "dns-rfc2136", "dns-route53"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -249,6 +249,8 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches req_auth = set_configurator(req_auth, "dns-luadns") if config.dns_nsone: req_auth = set_configurator(req_auth, "dns-nsone") + if config.dns_rfc2136: + req_auth = set_configurator(req_auth, "dns-rfc2136") if config.dns_route53: req_auth = set_configurator(req_auth, "dns-route53") logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) diff --git a/tools/release.sh b/tools/release.sh index 2ae0e630e..2a8e00aa1 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,8 +46,8 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-luadns certbot-dns-nsone certbot-dns-route53" -x +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53" + # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" SUBPKGS_NO_CERTBOT="$SUBPKGS_IN_AUTO_NO_CERTBOT $SUBPKGS_NOT_IN_AUTO" diff --git a/tools/venv.sh b/tools/venv.sh index 531057ec4..1533f0e1f 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -22,6 +22,7 @@ fi -e certbot-dns-google \ -e certbot-dns-luadns \ -e certbot-dns-nsone \ + -e certbot-dns-rfc2136 \ -e certbot-dns-route53 \ -e certbot-nginx \ -e letshelp-certbot \ diff --git a/tox.cover.sh b/tox.cover.sh index 76fc9f62c..fc0c9f476 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_route53 certbot_nginx letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx letshelp_certbot" else pkgs="$@" fi @@ -37,6 +37,8 @@ cover () { min=98 elif [ "$1" = "certbot_dns_nsone" ]; then min=99 + elif [ "$1" = "certbot_dns_rfc2136" ]; then + min=99 elif [ "$1" = "certbot_dns_route53" ]; then min=99 elif [ "$1" = "certbot_nginx" ]; then diff --git a/tox.ini b/tox.ini index 87962d6c4..61ffa06d7 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ py26_packages = certbot-dns-cloudflare \ certbot-dns-digitalocean \ certbot-dns-google \ + certbot-dns-rfc2136 \ certbot-dns-route53 \ certbot-nginx \ letshelp-certbot @@ -51,6 +52,7 @@ source_paths = certbot-dns-google/certbot_dns_google certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone + certbot-dns-rfc2136/certbot_dns_rfc2136 certbot-dns-route53/certbot_dns_route53 certbot-nginx/certbot_nginx letshelp-certbot/letshelp_certbot From ed717d6bc483dff49f3d6fe1f9d7ee973127ac29 Mon Sep 17 00:00:00 2001 From: Alexandre de Verteuil Date: Mon, 19 Jun 2017 12:39:14 -0400 Subject: [PATCH 063/125] tls-sni-01 with the manual plugin (#4636) * Add TLS-SNI-01 support to Manual plugin * Add environment variable CERTBOT_SNI_DOMAIN for manual-auth-hook * Make AuthenticatorTest inherit from TempDirTestCase * Add test_get_z_domain() * Document CERTBOT_SNI_DOMAIN in docs/using.rst --- certbot/plugins/common.py | 4 ++ certbot/plugins/common_test.py | 5 ++ certbot/plugins/manual.py | 90 +++++++++++++++++++++++++++++----- certbot/plugins/manual_test.py | 78 ++++++++++++++++++++++++----- docs/using.rst | 35 ++++++++----- 5 files changed, 175 insertions(+), 37 deletions(-) diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index aa58e86cc..255563bb6 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -241,6 +241,10 @@ class TLSSNI01(object): return os.path.join(self.configurator.config.work_dir, achall.chall.encode("token") + '.pem') + def get_z_domain(self, achall): + """Returns z_domain (SNI) name for the challenge.""" + return achall.response(achall.account_key).z_domain.decode("utf-8") + def _setup_challenge_cert(self, achall, cert_key=None): """Generate and write out challenge certificate.""" diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py index 3eedf92f7..411cbe651 100644 --- a/certbot/plugins/common_test.py +++ b/certbot/plugins/common_test.py @@ -221,6 +221,11 @@ class TLSSNI01Test(unittest.TestCase): mock_safe_open.return_value.write.assert_called_once_with( OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) + def test_get_z_domain(self): + achall = self.achalls[0] + self.assertEqual(self.sni.get_z_domain(achall), + achall.response(achall.account_key).z_domain.decode("utf-8")) + class InstallSslOptionsConfTest(test_util.TempDirTestCase): """Tests for certbot.plugins.common.install_ssl_options_conf.""" diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py index 093fec4be..07371ad34 100644 --- a/certbot/plugins/manual.py +++ b/certbot/plugins/manual.py @@ -9,9 +9,38 @@ from acme import challenges from certbot import interfaces from certbot import errors from certbot import hooks +from certbot import reverter from certbot.plugins import common +class ManualTlsSni01(common.TLSSNI01): + """TLS-SNI-01 authenticator for the Manual plugin + + :ivar configurator: Authenticator object + :type configurator: :class:`~certbot.plugins.manual.Authenticator` + + :ivar list achalls: Annotated + class:`~certbot.achallenges.KeyAuthorizationAnnotatedChallenge` + challenges + + :param list indices: Meant to hold indices of challenges in a + larger array. NginxTlsSni01 is capable of solving many challenges + at once which causes an indexing issue within NginxConfigurator + who must return all responses in order. Imagine NginxConfigurator + maintaining state about where all of the http-01 Challenges, + TLS-SNI-01 Challenges belong in the response array. This is an + optional utility. + + :param str challenge_conf: location of the challenge config file + """ + + def perform(self): + """Create the SSL certificates and private keys""" + + for achall in self.achalls: + self._setup_challenge_cert(achall) + + @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): @@ -28,14 +57,18 @@ class Authenticator(common.Plugin): 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.') + 'The environment variables available to this script depend on the ' + 'type of challenge. $CERTBOT_DOMAIN will always contain the domain ' + 'being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION ' + 'is the validation string, and $CERTBOT_TOKEN is the filename of the ' + 'resource requested when performing an HTTP-01 challenge. When ' + 'performing a TLS-SNI-01 challenge, $CERTBOT_SNI_DOMAIN will contain ' + 'the SNI name for which the ACME server expects to be presented with ' + 'the self-signed certificate located at $CERTBOT_CERT_PATH. The ' + 'secret key needed to complete the TLS handshake is located at ' + '$CERTBOT_KEY_PATH. 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: @@ -51,11 +84,22 @@ Create a file containing just this data: And make it available on your web server at this URL: {uri} +""" + _TLSSNI_INSTRUCTIONS = """\ +Configure the service listening on port {port} to present the certificate +{cert} +using the secret key +{key} +when it receives a TLS ClientHello with the SNI extension set to +{sni_domain} """ def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) + self.reverter = reverter.Reverter(self.config) + self.reverter.recovery_routine() self.env = dict() + self.tls_sni_01 = None @classmethod def add_parser_arguments(cls, add): @@ -90,11 +134,10 @@ And make it available on your web server at this URL: def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.HTTP01, challenges.DNS01] + return [challenges.HTTP01, challenges.DNS01, challenges.TLSSNI01] 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: @@ -102,6 +145,12 @@ And make it available on your web server at this URL: responses = [] for achall in achalls: + if isinstance(achall.chall, challenges.TLSSNI01): + # Make a new ManualTlsSni01 instance for each challenge + # because the manual plugin deals with one challenge at a time. + self.tls_sni_01 = ManualTlsSni01(self) + self.tls_sni_01.add_chall(achall) + self.tls_sni_01.perform() perform_achall(achall) responses.append(achall.response(achall.account_key)) return responses @@ -127,6 +176,16 @@ And make it available on your web server at this URL: env['CERTBOT_TOKEN'] = achall.chall.encode('token') else: os.environ.pop('CERTBOT_TOKEN', None) + if isinstance(achall.chall, challenges.TLSSNI01): + env['CERTBOT_CERT_PATH'] = self.tls_sni_01.get_cert_path(achall) + env['CERTBOT_KEY_PATH'] = self.tls_sni_01.get_key_path(achall) + env['CERTBOT_SNI_DOMAIN'] = self.tls_sni_01.get_z_domain(achall) + os.environ.pop('CERTBOT_VALIDATION', None) + env.pop('CERTBOT_VALIDATION') + else: + os.environ.pop('CERTBOT_CERT_PATH', None) + os.environ.pop('CERTBOT_KEY_PATH', None) + os.environ.pop('CERTBOT_SNI_DOMAIN', None) os.environ.update(env) _, out = hooks.execute(self.conf('auth-hook')) env['CERTBOT_AUTH_OUTPUT'] = out.strip() @@ -139,11 +198,17 @@ And make it available on your web server at this URL: achall=achall, encoded_token=achall.chall.encode('token'), port=self.config.http01_port, uri=achall.chall.uri(achall.domain), validation=validation) - else: - assert isinstance(achall.chall, challenges.DNS01) + elif isinstance(achall.chall, challenges.DNS01): msg = self._DNS_INSTRUCTIONS.format( domain=achall.validation_domain_name(achall.domain), validation=validation) + else: + assert isinstance(achall.chall, challenges.TLSSNI01) + msg = self._TLSSNI_INSTRUCTIONS.format( + cert=self.tls_sni_01.get_cert_path(achall), + key=self.tls_sni_01.get_key_path(achall), + port=self.config.tls_sni_01_port, + sni_domain=self.tls_sni_01.get_z_domain(achall)) display = zope.component.getUtility(interfaces.IDisplay) display.notification(msg, wrap=False, force_interactive=True) @@ -155,3 +220,4 @@ And make it available on your web server at this URL: os.environ.pop('CERTBOT_TOKEN', None) os.environ.update(env) hooks.execute(self.conf('cleanup-hook')) + self.reverter.recovery_routine() diff --git a/certbot/plugins/manual_test.py b/certbot/plugins/manual_test.py index bd6816f02..ac528e81c 100644 --- a/certbot/plugins/manual_test.py +++ b/certbot/plugins/manual_test.py @@ -13,17 +13,31 @@ from certbot.tests import acme_util from certbot.tests import util as test_util -class AuthenticatorTest(unittest.TestCase): +class AuthenticatorTest(test_util.TempDirTestCase): """Tests for certbot.plugins.manual.Authenticator.""" def setUp(self): + super(AuthenticatorTest, self).setUp() self.http_achall = acme_util.HTTP01_A self.dns_achall = acme_util.DNS01_A - self.achalls = [self.http_achall, self.dns_achall] + self.tls_sni_achall = acme_util.TLSSNI01_A + self.achalls = [self.http_achall, self.dns_achall, self.tls_sni_achall] + for d in ["config_dir", "work_dir", "in_progress"]: + os.mkdir(os.path.join(self.tempdir, d)) + # "backup_dir" and "temp_checkpoint_dir" get created in + # certbot.util.make_or_verify_dir() during the Reverter + # initialization. self.config = mock.MagicMock( http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None, manual_public_ip_logging_ok=False, noninteractive_mode=False, - validate_hooks=False) + validate_hooks=False, + config_dir=os.path.join(self.tempdir, "config_dir"), + work_dir=os.path.join(self.tempdir, "work_dir"), + backup_dir=os.path.join(self.tempdir, "backup_dir"), + temp_checkpoint_dir=os.path.join( + self.tempdir, "temp_checkpoint_dir"), + in_progress_dir=os.path.join(self.tempdir, "in_progess"), + tls_sni_01_port=5001) from certbot.plugins.manual import Authenticator self.auth = Authenticator(self.config, name='manual') @@ -42,7 +56,9 @@ class AuthenticatorTest(unittest.TestCase): def test_get_chall_pref(self): self.assertEqual(self.auth.get_chall_pref('example.org'), - [challenges.HTTP01, challenges.DNS01]) + [challenges.HTTP01, + challenges.DNS01, + challenges.TLSSNI01]) @test_util.patch_get_utility() def test_ip_logging_not_ok(self, mock_get_utility): @@ -58,13 +74,19 @@ class AuthenticatorTest(unittest.TestCase): 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( + 'echo ${CERTBOT_DOMAIN}; ' + 'echo ${CERTBOT_TOKEN:-notoken}; ' + 'echo ${CERTBOT_CERT_PATH:-nocert}; ' + 'echo ${CERTBOT_KEY_PATH:-nokey}; ' + 'echo ${CERTBOT_SNI_DOMAIN:-nosnidomain}; ' + 'echo ${CERTBOT_VALIDATION:-novalidation};') + dns_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( self.dns_achall.domain, 'notoken', + 'nocert', 'nokey', 'nosnidomain', self.dns_achall.validation(self.dns_achall.account_key)) - http_expected = '{0}\n{1}\n{2}'.format( + http_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( self.http_achall.domain, self.http_achall.chall.encode('token'), + 'nocert', 'nokey', 'nosnidomain', self.http_achall.validation(self.http_achall.account_key)) self.assertEqual( @@ -76,6 +98,17 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual( self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'], http_expected) + # tls_sni_01 challenge must be perform()ed above before we can + # get the cert_path and key_path. + tls_sni_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format( + self.tls_sni_achall.domain, 'notoken', + self.auth.tls_sni_01.get_cert_path(self.tls_sni_achall), + self.auth.tls_sni_01.get_key_path(self.tls_sni_achall), + self.auth.tls_sni_01.get_z_domain(self.tls_sni_achall), + 'novalidation') + self.assertEqual( + self.auth.env[self.tls_sni_achall.domain]['CERTBOT_AUTH_OUTPUT'], + tls_sni_expected) @test_util.patch_get_utility() def test_manual_perform(self, mock_get_utility): @@ -85,7 +118,13 @@ class AuthenticatorTest(unittest.TestCase): [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]) + if isinstance(achall.chall, challenges.TLSSNI01): + self.assertTrue( + self.auth.tls_sni_01.get_cert_path( + self.tls_sni_achall) in args[0]) + else: + self.assertTrue( + achall.validation(achall.account_key) in args[0]) self.assertFalse(kwargs['wrap']) def test_cleanup(self): @@ -98,16 +137,29 @@ class AuthenticatorTest(unittest.TestCase): 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)) - + if (isinstance(achall.chall, challenges.HTTP01) or + isinstance(achall.chall, challenges.DNS01)): + self.assertEqual( + os.environ['CERTBOT_VALIDATION'], + achall.validation(achall.account_key)) 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 isinstance(achall.chall, challenges.TLSSNI01): + self.assertEqual( + os.environ['CERTBOT_CERT_PATH'], + self.auth.tls_sni_01.get_cert_path(achall)) + self.assertEqual( + os.environ['CERTBOT_KEY_PATH'], + self.auth.tls_sni_01.get_key_path(achall)) + self.assertFalse( + os.path.exists(os.environ['CERTBOT_CERT_PATH'])) + self.assertFalse( + os.path.exists(os.environ['CERTBOT_KEY_PATH'])) + if __name__ == '__main__': diff --git a/docs/using.rst b/docs/using.rst index 4bbf81d50..3b76c3e06 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -54,9 +54,9 @@ standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. | Requires port 80 or 443 to be available. This is useful on tls-sni-01_ (443) | systems with no webserver, or when direct integration with | the local webserver is not supported or not desired. -manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80) or - | perform domain validation yourself. Additionally allows you dns-01_ (53) - | to specify scripts to automate the validation task in a +manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80), + | perform domain validation yourself. Additionally allows you dns-01_ (53) or + | to specify scripts to automate the validation task in a tls-sni-01_ (443) | customized way. =========== ==== ==== =============================================================== ============================= @@ -175,13 +175,15 @@ the UI, you can use the plugin to obtain a certificate by specifying to copy and paste commands into another terminal session, which may be on a different computer. -The manual plugin can use either the ``http`` or the ``dns`` challenge. You -can use the ``--preferred-challenges`` option to choose the challenge of your -preference. +The manual plugin can use either the ``http``, ``dns`` or the +``tls-sni`` challenge. You can use the ``--preferred-challenges`` option +to choose the challenge of your preference. + The ``http`` challenge will ask you to place a file with a specific name and specific content in the ``/.well-known/acme-challenge/`` directory directly in the top-level directory (“web root”) containing the files served by your webserver. In essence it's the same as the webroot_ plugin, but not automated. + When using the ``dns`` challenge, ``certbot`` will ask you to place a TXT DNS record with specific contents under the domain name consisting of the hostname for which you want a certificate issued, prepended by ``_acme-challenge``. @@ -192,10 +194,16 @@ For example, for the domain ``example.com``, a zone file entry would look like: _acme-challenge.example.com. 300 IN TXT "gfj9Xq...Rg85nM" -Additionally you can specify scripts to prepare for validation and perform the -authentication procedure and/or clean up after it by using the -``--manual-auth-hook`` and ``--manual-cleanup-hook`` flags. This is described in -more depth in the hooks_ section. +When using the ``tls-sni`` challenge, ``certbot`` will prepare a self-signed +SSL certificate for you with the challenge validation appropriately +encoded into a subjectAlternatNames entry. You will need to configure +your SSL server to present this challenge SSL certificate to the ACME +server using SNI. + +Additionally you can specify scripts to prepare for validation and +perform the authentication procedure and/or clean up after it by using +the ``--manual-auth-hook`` and ``--manual-cleanup-hook`` flags. This is +described in more depth in the hooks_ section. .. _third-party-plugins: @@ -606,12 +614,15 @@ and ``--manual-cleanup-hook`` respectively and can be used as follows: certbot certonly --manual --manual-auth-hook /path/to/http/authenticator.sh --manual-cleanup-hook /path/to/http/cleanup.sh -d secure.example.com This will run the ``authenticator.sh`` script, attempt the validation, and then run -the ``cleanup.sh`` script. Additionally certbot will pass three environment +the ``cleanup.sh`` script. Additionally certbot will pass relevant environment variables to these scripts: - ``CERTBOT_DOMAIN``: The domain being authenticated -- ``CERTBOT_VALIDATION``: The validation string +- ``CERTBOT_VALIDATION``: The validation string (HTTP-01 and DNS-01 only) - ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenge (HTTP-01 only) +- ``CERTBOT_CERT_PATH``: The challenge SSL certificate (TLS-SNI-01 only) +- ``CERTBOT_KEY_PATH``: The private key associated with the aforementioned SSL certificate (TLS-SNI-01 only) +- ``CERTBOT_SNI_DOMAIN``: The SNI name for which the ACME server expects to be presented the self-signed certificate located at ``$CERTBOT_CERT_PATH`` (TLS-SNI-01 only) Additionally for cleanup: From be457ffa95864afb223985bc950644386b86e6a2 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Thu, 29 Sep 2016 15:57:29 -0700 Subject: [PATCH 064/125] Test more in nginx compatibility tests * Highlight failures more with asterisks * Filter out wildcard names from all_names * Only test -ai, not -aie (no redirects) * Modified versions of almost all of 79 configs corpus * Re-enable now-working stanza with 301 redirect * Change another redirect to go to :443 --- .../configurators/nginx/common.py | 4 +++- .../certbot_compatibility_test/test_driver.py | 20 +++++++++--------- .../testdata/nginx.tar.gz | Bin 6625 -> 39666 bytes tox.ini | 2 +- 4 files changed, 14 insertions(+), 12 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 3622bee41..ed5cf750e 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -79,6 +79,8 @@ def _get_names(config): if line.strip().startswith("server_name"): names = line.partition("server_name")[2].rpartition(";")[0] for n in names.split(): - all_names.add(n) + # Filter out wildcards in both all_names and test_names + if not n.startswith("*."): + 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 diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 71100bb27..c07586d8c 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -74,7 +74,7 @@ def test_authenticator(plugin, config, temp_dir): "tls-sni-01 verification for %s succeeded", achalls[i].domain) else: logger.error( - "tls-sni-01 verification for %s in %s failed", + "**** tls-sni-01 verification for %s in %s failed", achalls[i].domain, config) success = False @@ -122,7 +122,7 @@ def test_installer(args, plugin, config, temp_dir): if names_match: logger.info("get_all_names test succeeded") else: - logger.error("get_all_names test failed for config %s", config) + logger.error("**** get_all_names test failed for config %s", config) domains = list(plugin.get_testable_domain_names()) success = test_deploy_cert(plugin, temp_dir, domains) @@ -147,7 +147,7 @@ def test_deploy_cert(plugin, temp_dir, domains): 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 certificate for %s:", domain) + logger.error("**** Plugin failed to deploy certificate for %s:", domain) logger.exception(error) return False @@ -159,7 +159,7 @@ def test_deploy_cert(plugin, temp_dir, domains): verify = functools.partial(validator.Validator().certificate, cert, domain, "127.0.0.1", plugin.https_port) if not _try_until_true(verify): - logger.error("Could not verify certificate for domain %s", domain) + logger.error("**** Could not verify certificate for domain %s", domain) success = False if success: @@ -183,10 +183,10 @@ def test_enhancements(plugin, domains): plugin.save() # Needed by the Apache plugin except le_errors.PluginError as error: # Don't immediately fail because a redirect may already be enabled - logger.warning("Plugin failed to enable redirect for %s:", domain) + logger.warning("*** Plugin failed to enable redirect for %s:", domain) logger.warning("%s", error) except le_errors.Error as error: - logger.error("An error occurred while enabling redirect for %s:", + logger.error("*** An error occurred while enabling redirect for %s:", domain) logger.exception(error) @@ -198,7 +198,7 @@ def test_enhancements(plugin, domains): verify = functools.partial(validator.Validator().redirect, "localhost", plugin.http_port, headers={"Host": domain}) if not _try_until_true(verify): - logger.error("Improper redirect for domain %s", domain) + logger.error("*** Improper redirect for domain %s", domain) success = False if success: @@ -225,7 +225,7 @@ def _save_and_restart(plugin, title=None): plugin.restart() return True except le_errors.Error as error: - logger.error("Plugin failed to save and restart server:") + logger.error("*** Plugin failed to save and restart server:") logger.exception(error) return False @@ -235,12 +235,12 @@ def test_rollback(plugin, config, backup): try: plugin.rollback_checkpoints(1337) except le_errors.Error as error: - logger.error("Plugin raised an exception during rollback:") + logger.error("*** Plugin raised an exception during rollback:") logger.exception(error) return False if _dirs_are_unequal(config, backup): - logger.error("Rollback failed for config `%s`", config) + logger.error("*** Rollback failed for config `%s`", config) return False else: logger.info("Rollback succeeded") diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz index 4ca5cc9775a35f64534f90b1db4fb7ef272672a8..0caf3c3be232d4fae8d467fc6f3325147d323b45 100644 GIT binary patch literal 39666 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 diff --git a/tox.ini b/tox.ini index 61ffa06d7..5d6de5730 100644 --- a/tox.ini +++ b/tox.ini @@ -151,7 +151,7 @@ passenv = DOCKER_* 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 + docker run --rm -it nginx-compat -c nginx.tar.gz -vvvv -ai whitelist_externals = docker passenv = DOCKER_* From 15c6c1388eb7bca2b290f1f6fcc51e89c9bb1810 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Fri, 30 Sep 2016 21:55:52 +0000 Subject: [PATCH 065/125] Have validator only test domains without existing redirects --- .../certbot_compatibility_test/test_driver.py | 23 +++++++++++++------ .../certbot_compatibility_test/validator.py | 19 ++++++++------- tox.ini | 2 +- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index c07586d8c..928870f66 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -177,8 +177,15 @@ def test_enhancements(plugin, domains): "enhancements") return False - for domain in domains: + domains_and_info = [(domain, []) for domain in domains] + + for domain, info in domains_and_info: try: + verify = functools.partial(validator.Validator().any_redirect, + "localhost", plugin.http_port, + headers={"Host": domain}) + previous_redirect = _try_until_true(verify) + info.append(previous_redirect) plugin.enhance(domain, "redirect") plugin.save() # Needed by the Apache plugin except le_errors.PluginError as error: @@ -194,12 +201,14 @@ def test_enhancements(plugin, domains): return False success = True - for domain in domains: - verify = functools.partial(validator.Validator().redirect, "localhost", - plugin.http_port, headers={"Host": domain}) - if not _try_until_true(verify): - logger.error("*** Improper redirect for domain %s", domain) - success = False + for domain, info in domains_and_info: + previous_redirect = info[0] + if not previous_redirect: + verify = functools.partial(validator.Validator().redirect, "localhost", + plugin.http_port, headers={"Host": domain}) + if not _try_until_true(verify): + logger.error("*** Improper redirect for domain %s", domain) + success = False if success: logger.info("Enhancements test succeeded") diff --git a/certbot-compatibility-test/certbot_compatibility_test/validator.py b/certbot-compatibility-test/certbot_compatibility_test/validator.py index 62dd466a1..0fd6efab5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/validator.py +++ b/certbot-compatibility-test/certbot_compatibility_test/validator.py @@ -45,19 +45,12 @@ class Validator(object): else: response = requests.get(url, allow_redirects=False) - if response.status_code not in (301, 303): - 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://"): - if not redirect_location.startswith("http://"): - return False - else: - if redirect_location[len("http://"):] == name: - return False + return False if response.status_code != 301: logger.error("Server did not redirect with permanent code") @@ -65,6 +58,16 @@ class Validator(object): return True + def any_redirect(self, name, port=80, headers=None): + """Test whether webserver redirects.""" + url = "http://{0}:{1}".format(name, port) + if headers: + response = requests.get(url, headers=headers, allow_redirects=False) + else: + response = requests.get(url, allow_redirects=False) + + return response.status_code in xrange(300, 309) + def hsts(self, name): """Test for HTTP Strict Transport Security header""" headers = requests.get("https://" + name).headers diff --git a/tox.ini b/tox.ini index 5d6de5730..39218651c 100644 --- a/tox.ini +++ b/tox.ini @@ -151,7 +151,7 @@ passenv = DOCKER_* 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 -ai + docker run --rm -it nginx-compat -c nginx.tar.gz -vvvv -aie whitelist_externals = docker passenv = DOCKER_* From bdf02c9fccd66c88f5e8f81174409c584d804bf8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 21 Jun 2017 11:46:30 -0700 Subject: [PATCH 066/125] Turn on IRC notifications for Travis failures in master --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 58618e5a0..55ca41f4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -180,4 +180,3 @@ notifications: on_success: never on_failure: always use_notice: true - skip_join: true From 6aa21d1db6889025f80e66a73d349a9fc1491362 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 20 Jun 2017 14:02:55 -0700 Subject: [PATCH 067/125] Fix and speed up compatibility-tests * Fix nginx-compatibility tests * sleep is overrated * Reduce verbosity of nginx tests --- .../certbot_compatibility_test/test_driver.py | 43 ++++++------------ .../testdata/nginx.tar.gz | Bin 39666 -> 39463 bytes tox.ini | 2 +- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 928870f66..71a0ba574 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -1,7 +1,6 @@ """Tests Certbot plugins against different server configurations.""" import argparse import filecmp -import functools import logging import os import shutil @@ -64,12 +63,12 @@ def test_authenticator(plugin, config, temp_dir): type(achalls[i]), achalls[i].domain, config) success = False elif isinstance(responses[i], challenges.TLSSNI01Response): - verify = functools.partial(responses[i].simple_verify, achalls[i].chall, - achalls[i].domain, - util.JWK.public_key(), - host="127.0.0.1", - port=plugin.https_port) - if _try_until_true(verify): + verified = responses[i].simple_verify(achalls[i].chall, + achalls[i].domain, + util.JWK.public_key(), + host="127.0.0.1", + port=plugin.https_port) + if verified: logger.info( "tls-sni-01 verification for %s succeeded", achalls[i].domain) else: @@ -155,10 +154,11 @@ def test_deploy_cert(plugin, temp_dir, domains): return False success = True + time.sleep(3) for domain in domains: - verify = functools.partial(validator.Validator().certificate, cert, - domain, "127.0.0.1", plugin.https_port) - if not _try_until_true(verify): + verified = validator.Validator().certificate( + cert, domain, "127.0.0.1", plugin.https_port) + if not verified: logger.error("**** Could not verify certificate for domain %s", domain) success = False @@ -181,10 +181,8 @@ def test_enhancements(plugin, domains): for domain, info in domains_and_info: try: - verify = functools.partial(validator.Validator().any_redirect, - "localhost", plugin.http_port, - headers={"Host": domain}) - previous_redirect = _try_until_true(verify) + previous_redirect = validator.Validator().any_redirect( + "localhost", plugin.http_port, headers={"Host": domain}) info.append(previous_redirect) plugin.enhance(domain, "redirect") plugin.save() # Needed by the Apache plugin @@ -204,9 +202,9 @@ def test_enhancements(plugin, domains): for domain, info in domains_and_info: previous_redirect = info[0] if not previous_redirect: - verify = functools.partial(validator.Validator().redirect, "localhost", - plugin.http_port, headers={"Host": domain}) - if not _try_until_true(verify): + verified = validator.Validator().redirect( + "localhost", plugin.http_port, headers={"Host": domain}) + if not verified: logger.error("*** Improper redirect for domain %s", domain) success = False @@ -216,17 +214,6 @@ def test_enhancements(plugin, domains): return success -def _try_until_true(func, max_tries=5, sleep_time=0.5): - """Calls func up to max_tries times until it returns True""" - for _ in xrange(0, max_tries): - if func(): - return True - else: - time.sleep(sleep_time) - - return False - - def _save_and_restart(plugin, title=None): """Saves and restart the plugin, returning True if no errors occurred""" try: diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/nginx.tar.gz index 0caf3c3be232d4fae8d467fc6f3325147d323b45..2f06add17075efb5d096cdd69d447b759b5ef75f 100644 GIT binary patch literal 39463 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 Date: Wed, 21 Jun 2017 13:49:15 -0700 Subject: [PATCH 068/125] Test additional subcommands in integration tests (#4855) * Test additional subcommands * Test rollback * quote original variable * Specifically set installer to work around #4858. --- certbot-nginx/tests/boulder-integration.sh | 11 +++++++-- tests/boulder-integration.sh | 27 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/tests/boulder-integration.sh b/certbot-nginx/tests/boulder-integration.sh index bd35aee21..996cc2201 100755 --- a/certbot-nginx/tests/boulder-integration.sh +++ b/certbot-nginx/tests/boulder-integration.sh @@ -6,14 +6,18 @@ 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 +original=$(root="$nginx_root" ./certbot-nginx/tests/boulder-integration.conf.sh) +nginx_conf="$nginx_root/nginx.conf" +echo "$original" > $nginx_conf + killall nginx || true nginx -c $nginx_root/nginx.conf certbot_test_nginx () { certbot_test \ - --configurator nginx \ + --authenticator nginx \ + --installer nginx \ --nginx-server-root $nginx_root \ "$@" } @@ -23,6 +27,9 @@ echo | openssl s_client -connect localhost:5001 \ | openssl x509 -out $root/nginx.pem diff -q $root/nginx.pem $root/conf/live/nginx.wtf/cert.pem +certbot_test_nginx rollback --checkpoints 9001 +diff -q <(echo "$original") $nginx_conf + # note: not reached if anything above fails, hence "killall" at the # top nginx -c $nginx_root/nginx.conf -s stop diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index ddbaa43ed..42a8cf499 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -88,6 +88,11 @@ if [ $(get_num_tmp_files) -ne $num_tmp_files ]; then exit 1 fi +common register +common register --update-registration --email example@example.org + +common plugins --init --prepare | grep webroot + # We start a server listening on the port for the # unrequested challenge to prevent regressions in #3601. python ./tests/run_http_server.py $http_01_port & @@ -211,6 +216,28 @@ common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \ common unregister +out=$(common certificates) +subdomains="le le2 dns.le newname.le must-staple.le" +for subdomain in $subdomains; do + domain="$subdomain.wtf" + if ! echo $out | grep "$domain"; then + echo "$domain not in certificates output!" + exit 1; + fi +done + +cert_name="must-staple.le.wtf" +common delete --cert-name $cert_name +archive="$root/conf/archive/$cert_name" +conf="$root/conf/renewal/$cert_name.conf" +live="$root/conf/live/$cert_name" +for path in $archive $conf $live; do + if [ -e $path ]; then + echo "Lineage not properly deleted!" + exit 1 + fi +done + # Most CI systems set this variable to true. # If the tests are running as part of CI, Nginx should be available. if ${CI:-false} || type nginx; From 1e6ea09dbd8e3ea624791185fbd0bca364a54e97 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 21 Jun 2017 13:50:48 -0700 Subject: [PATCH 069/125] Remove py26 oldest tests (#4856) * Remove py26 oldest tests. The only systems where we support Python 2.6 use certbot-auto so the oldest supported versions of our dependencies are never used when using supported installation methods. Let's remove this unnecessary and slow test. * Make tox.ini happy * Remove py26-oldest from Travis --- .travis.yml | 7 ------- tox.ini | 27 ++++++++++++--------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 58618e5a0..de17c0402 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,13 +33,6 @@ matrix: - sudo cat /var/log/mysql/error.log - ps aux | grep mysql services: docker - - python: "2.6" - env: TOXENV=py26-oldest BOULDER_INTEGRATION=1 - sudo: required - after_failure: - - 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 diff --git a/tox.ini b/tox.ini index 61ffa06d7..a1c1839e2 100644 --- a/tox.ini +++ b/tox.ini @@ -63,9 +63,20 @@ commands = {[base]install_and_test} {[base]py26_packages} python tests/lock_test.py -[testenv:py26-oldest] +[testenv] commands = {[testenv:py26]commands} + {[base]install_and_test} {[base]non_py26_packages} +setenv = + PYTHONPATH = {toxinidir} + PYTHONHASHSEED = 0 + +[testenv:py27-oldest] +commands = + {[testenv]commands} +setenv = + {[testenv]setenv} + CERTBOT_NO_PIN=1 # 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 oldest tests due to @@ -78,20 +89,6 @@ deps = requests<=2.11.1 zope.component==4.0.2 zope.interface==4.0.5 -setenv = - CERTBOT_NO_PIN=1 - -[testenv] -commands = - {[testenv:py26]commands} - {[base]install_and_test} {[base]non_py26_packages} -setenv = - PYTHONPATH = {toxinidir} - PYTHONHASHSEED = 0 - py27-oldest: {[testenv:py26-oldest]setenv} -# https://testrun.org/tox/latest/example/basic.html#special-handling-of-pythonhas -deps = - py27-oldest: {[testenv:py26-oldest]deps} [testenv:py27_install] basepython = python2.7 From 3cb92d33eb03c91819324416048fdc7efa097814 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 21 Jun 2017 14:10:16 -0700 Subject: [PATCH 070/125] report and enforce coverage on integration tests (#4854) --- tests/boulder-integration.sh | 5 +++++ tests/integration/_common.sh | 38 ++++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 42a8cf499..cf49f664b 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -74,6 +74,9 @@ CheckHooks() { rm "$HOOK_TEST" } +# Cleanup coverage data +coverage erase + # test for regressions of #4719 get_num_tmp_files() { ls -1 /tmp | wc -l @@ -244,3 +247,5 @@ if ${CI:-false} || type nginx; then . ./certbot-nginx/tests/boulder-integration.sh fi + +coverage report --fail-under 52 -m diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 8d4baff95..48d20eb3b 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -6,7 +6,8 @@ store_flags="--config-dir $root/conf --work-dir $root/work" store_flags="$store_flags --logs-dir $root/logs" tls_sni_01_port=5001 http_01_port=5002 -export root store_flags tls_sni_01_port http_01_port +sources="acme/,$(ls -dm certbot*/ | tr -d ' \n')" +export root store_flags tls_sni_01_port http_01_port sources certbot_test () { certbot_test_no_force_renew \ @@ -15,18 +16,25 @@ certbot_test () { } certbot_test_no_force_renew () { - certbot \ - --server "${SERVER:-http://localhost:4000/directory}" \ - --no-verify-ssl \ - --tls-sni-01-port $tls_sni_01_port \ - --http-01-port $http_01_port \ - --manual-public-ip-logging-ok \ - $store_flags \ - --non-interactive \ - --no-redirect \ - --agree-tos \ - --register-unsafely-without-email \ - --debug \ - -vv \ - "$@" + omit_patterns="*/*.egg-info/*,*/dns_common*,*/setup.py,*/test_*,*/tests/*" + omit_patterns="$omit_patterns,*_test.py,*_test_*," + omit_patterns="$omit_patterns,certbot-compatibility-test/*,certbot-dns*/" + coverage run \ + --append \ + --source $sources \ + --omit $omit_patterns \ + $(command -v certbot) \ + --server "${SERVER:-http://localhost:4000/directory}" \ + --no-verify-ssl \ + --tls-sni-01-port $tls_sni_01_port \ + --http-01-port $http_01_port \ + --manual-public-ip-logging-ok \ + $store_flags \ + --non-interactive \ + --no-redirect \ + --agree-tos \ + --register-unsafely-without-email \ + --debug \ + -vv \ + "$@" } From 077aea5fb1161d7e096fef0893a0662e561d9697 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 21 Jun 2017 14:23:19 -0700 Subject: [PATCH 071/125] add documentation for lock file (#4862) --- docs/using.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 3b76c3e06..0b2206db2 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -722,7 +722,28 @@ Example usage for DNS-01 (Cloudflare API v4) (for example purposes only, do not fi fi +.. _lock-files: +Lock Files +========== + +When processing a validation Certbot writes a number of lock files on your system +to prevent multiple instances from overwriting each other's changes. This means +that be default two instances of Certbot will not be able to run in parallel. + +Since the directories used by Certbot are configurable, Certbot +will write a lock file for all of the directories it uses. This include Certbot's +``--work-dir``, ``--logs-dir``, and ``--config-dir``. By default these are +``/var/lib/letsencrypt``, ``/var/logs/letsencrypt``, and ``/etc/letsencrypt`` +respectively. Additionally if you are using Certbot with Apache or nginx it will +lock the configuration folder for that program, which are typically also in the +``/etc`` directory. + +Note that these lock files will only prevent other instances of Certbot from +using those directories, not other processes. If you'd like to run multiple +instances of Certbot simultaneously you should specify different directories +as the ``--work-dir``, ``--logs-dir``, and ``--config-dir`` for each instance +of Certbot that you would like to run. .. _config-file: From 03f6c6d0e5353490fbde01f55c7ecdbbde9acd84 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 21 Jun 2017 21:08:37 -0700 Subject: [PATCH 072/125] Bump min integration test coverage to 64 (#4868) --- tests/boulder-integration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index cf49f664b..913880c8b 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -248,4 +248,4 @@ then . ./certbot-nginx/tests/boulder-integration.sh fi -coverage report --fail-under 52 -m +coverage report --fail-under 64 -m From f4094e4d3f6edefd790863e1fba22a908e4e708f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 23 Jun 2017 09:40:59 -0700 Subject: [PATCH 073/125] Finish oldest tests (#4857) * Pin oldest version of packaged python deps * Install security extras in oldest tests * Revert "bump requests requirement to >=2.10 (#4248)" This reverts commit 402ad8b35311460babb7195095b10d02a0b14e48. * Use create=True when patching open on module --- acme/setup.py | 6 +--- .../certbot_dns_google/dns_google_test.py | 18 ++++++------ tox.ini | 29 ++++++++++++++----- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 76ca87afa..4e6eaf50c 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -16,11 +16,7 @@ install_requires = [ 'PyOpenSSL>=0.13', 'pyrfc3339', 'pytz', - # requests>=2.10 is required to fix - # https://github.com/shazow/urllib3/issues/556. This requirement can be - # relaxed to 'requests[security]>=2.4.1', however, less useful errors - # will be raised for some network/SSL errors. - 'requests[security]>=2.10', + '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/certbot-dns-google/certbot_dns_google/dns_google_test.py b/certbot-dns-google/certbot_dns_google/dns_google_test.py index eb41fa4ee..95e3347a1 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google_test.py +++ b/certbot-dns-google/certbot_dns_google/dns_google_test.py @@ -76,7 +76,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) @@ -101,7 +101,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record_and_poll(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) changes.create.return_value.execute.return_value = {'status': 'pending', 'id': self.change} @@ -119,7 +119,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record_error_during_zone_lookup(self, unused_credential_mock): client, unused_changes = self._setUp_client_with_mock(API_ERROR) @@ -128,7 +128,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record_zone_not_found(self, unused_credential_mock): client, unused_changes = self._setUp_client_with_mock([{'managedZones': []}, {'managedZones': []}]) @@ -138,7 +138,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_add_txt_record_error_during_add(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) changes.create.side_effect = API_ERROR @@ -148,7 +148,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_del_txt_record(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) @@ -173,7 +173,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_del_txt_record_error_during_zone_lookup(self, unused_credential_mock): client, unused_changes = self._setUp_client_with_mock(API_ERROR) @@ -181,7 +181,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_del_txt_record_zone_not_found(self, unused_credential_mock): client, unused_changes = self._setUp_client_with_mock([{'managedZones': []}, {'managedZones': []}]) @@ -190,7 +190,7 @@ class GoogleClientTest(unittest.TestCase): @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', - mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}')) + mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'), create=True) def test_del_txt_record_error_during_delete(self, unused_credential_mock): client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}]) changes.create.side_effect = API_ERROR diff --git a/tox.ini b/tox.ini index a1c1839e2..7e7181528 100644 --- a/tox.ini +++ b/tox.ini @@ -77,17 +77,30 @@ commands = setenv = {[testenv]setenv} CERTBOT_NO_PIN=1 -# 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 oldest tests due to -# https://github.com/shazow/urllib3/pull/930 deps = - cffi<=1.7 - cryptography==1.2 - configargparse==0.10.0 PyOpenSSL==0.13 - requests<=2.11.1 + cffi==1.5.2 + configargparse==0.10.0 + configargparse==0.10.0 + configobj==4.7.2 + cryptography==1.2.3 + enum34==0.9.23 + idna==2.0 + ipaddress==1.0.16 + mock==1.0.1 + ndg-httpsclient==0.3.2 + parsedatetime==1.4 + pyasn1==0.1.9 + pyparsing==1.5.6 + pyrfc3339==1.0 + python-augeas==0.4.1 + pytz==2012c + requests[security]==2.6.0 + setuptools==0.9.8 + six==1.9.0 + urllib3==1.10 zope.component==4.0.2 + zope.event==4.0.1 zope.interface==4.0.5 [testenv:py27_install] From 33306de90b9c4825e20d86e5d2ba77bbcf21a3bc Mon Sep 17 00:00:00 2001 From: "T.C. Hollingsworth" Date: Mon, 26 Jun 2017 18:44:06 -0700 Subject: [PATCH 074/125] docs: explain how to combine plugins --- docs/using.rst | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 0b2206db2..e559e7a98 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -39,7 +39,7 @@ 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. +a combination_ of distinct authenticator and installer plugins. =========== ==== ==== =============================================================== ============================= Plugin Auth Inst Notes Challenge types (and port) @@ -205,6 +205,26 @@ perform the authentication procedure and/or clean up after it by using the ``--manual-auth-hook`` and ``--manual-cleanup-hook`` flags. This is described in more depth in the hooks_ section. +.. _combination: + +Combining plugins +----------------- + +Sometimes you may want to specify a combination of distinct authenticator and +installer plugins. To do so, specify the authenticator plugin with +``--authenticator`` or ``-a`` and the installer plugin with ``--installer`` or +``-i``. + +For instance, you may want to create a certificate using the webroot_ plugin +for authentication and the apache_ plugin for installation, perhaps because you +use a proxy or CDN for SSL and only want to secure the connection between them +and your origin server, which cannot use the tls-sni-01_ challenge due to the +intermediate proxy.) + +:: + + certbot run -a webroot -i apache -w /var/www/html -d example.com + .. _third-party-plugins: Third-party plugins From 7d17919527ef0cb96990f78fc4a7a777063d0eeb Mon Sep 17 00:00:00 2001 From: "T.C. Hollingsworth" Date: Tue, 27 Jun 2017 18:12:05 -0700 Subject: [PATCH 075/125] docs: remove errant parenthesis --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index e559e7a98..59c41b110 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -219,7 +219,7 @@ For instance, you may want to create a certificate using the webroot_ plugin for authentication and the apache_ plugin for installation, perhaps because you use a proxy or CDN for SSL and only want to secure the connection between them and your origin server, which cannot use the tls-sni-01_ challenge due to the -intermediate proxy.) +intermediate proxy. :: From 828363b21ae5b9d9b5735db343667b1822853817 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 08:10:55 -0400 Subject: [PATCH 076/125] Fix nginx --dry-run (#4889) * Revert "Don't save keys/csr on dry run (#4380)" This reverts commit e034b50363a75d61d1a7e7add2b8aca52684acd1. * Don't save CSRs and keys during dry run * Factor out _test_obtain_certificate_common * Add test_obtain_certificate_dry_run * Wrap key from make_key in util.Key * Wrap result from make_csr in util.CSR --- certbot-nginx/certbot_nginx/tests/util.py | 1 - certbot/client.py | 15 +++++-- certbot/constants.py | 1 - certbot/crypto_util.py | 28 +++++-------- certbot/tests/client_test.py | 49 ++++++++++++++++------- certbot/tests/crypto_util_test.py | 34 ++-------------- 6 files changed, 60 insertions(+), 68 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 2ee38ec38..6e1b0d8ff 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -65,7 +65,6 @@ def get_nginx_configurator( in_progress_dir=os.path.join(backups, "IN_PROGRESS"), server="https://acme-server.org:443/new", tls_sni_01_port=5001, - dry_run=False, ), name="nginx", version=version) diff --git a/certbot/client.py b/certbot/client.py index 7896ab7dc..6010dd0a0 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -9,6 +9,7 @@ import OpenSSL import zope.component from acme import client as acme_client +from acme import crypto_util as acme_crypto_util from acme import errors as acme_errors from acme import jose from acme import messages @@ -319,9 +320,17 @@ class Client(object): domains = [d for d in domains if d in auth_domains] # Create CSR from names - key = crypto_util.init_save_key( - self.config.rsa_key_size, self.config.key_dir) - csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) + if self.config.dry_run: + key = util.Key(file=None, + pem=crypto_util.make_key(self.config.rsa_key_size)) + csr = util.CSR(file=None, form="pem", + data=acme_crypto_util.make_csr( + key.pem, domains, self.config.must_staple)) + else: + key = crypto_util.init_save_key( + self.config.rsa_key_size, self.config.key_dir) + csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) + certr, chain = self.obtain_certificate_from_csr( domains, csr, authzr=authzr) diff --git a/certbot/constants.py b/certbot/constants.py index 7919b3317..dfdfcc0e8 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -18,7 +18,6 @@ CLI_DEFAULTS = dict( os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), ], - dry_run=False, verbose_count=-int(logging.INFO / 10), server="https://acme-v01.api.letsencrypt.org/directory", rsa_key_size=2048, diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 0ac0eee40..e22effeb7 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -55,15 +55,11 @@ 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) - if config.dry_run: - key_path = None - logger.debug("Generating key (%d bits), not saving to file", key_size) - else: - key_f, key_path = util.unique_file( - os.path.join(key_dir, keyname), 0o600, "wb") - with key_f: - key_f.write(key_pem) - logger.debug("Generating key (%d bits): %s", key_size, key_path) + key_f, key_path = util.unique_file( + os.path.join(key_dir, keyname), 0o600, "wb") + with key_f: + key_f.write(key_pem) + logger.debug("Generating key (%d bits): %s", key_size, key_path) return util.Key(key_path, key_pem) @@ -90,15 +86,11 @@ def init_save_csr(privkey, names, path): # Save CSR util.make_or_verify_dir(path, 0o755, os.geteuid(), config.strict_permissions) - if config.dry_run: - csr_filename = None - logger.debug("Creating CSR: not saving to file") - else: - csr_f, csr_filename = util.unique_file( - os.path.join(path, "csr-certbot.pem"), 0o644, "wb") - with csr_f: - csr_f.write(csr_pem) - logger.debug("Creating CSR: %s", csr_filename) + csr_f, csr_filename = util.unique_file( + os.path.join(path, "csr-certbot.pem"), 0o644, "wb") + with csr_f: + csr_f.write(csr_pem) + logger.debug("Creating CSR: %s", csr_filename) return util.CSR(csr_filename, csr_pem, "pem") diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 391a407ac..97fd6241d 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -144,6 +144,7 @@ class ClientTest(ClientTestCommon): self.config.allow_subset_of_names = False self.config.config_dir = "/etc/letsencrypt" + self.config.dry_run = False self.eg_domains = ["example.com", "www.example.com"] def test_init_acme_verify_ssl(self): @@ -241,15 +242,37 @@ class ClientTest(ClientTestCommon): self.assertEqual(1, mock_get_utility().notification.call_count) @mock.patch("certbot.client.crypto_util") - @test_util.patch_get_utility() - def test_obtain_certificate(self, unused_mock_get_utility, - mock_crypto_util): - self._mock_obtain_certificate() - + def test_obtain_certificate(self, mock_crypto_util): csr = util.CSR(form="pem", file=None, data=CSR_SAN) mock_crypto_util.init_save_csr.return_value = csr mock_crypto_util.init_save_key.return_value = mock.sentinel.key - domains = ["example.com", "www.example.com"] + + self._test_obtain_certificate_common(mock.sentinel.key, csr) + + mock_crypto_util.init_save_key.assert_called_once_with( + self.config.rsa_key_size, self.config.key_dir) + mock_crypto_util.init_save_csr.assert_called_once_with( + mock.sentinel.key, self.eg_domains, self.config.csr_dir) + + @mock.patch("certbot.client.crypto_util") + @mock.patch("certbot.client.acme_crypto_util") + def test_obtain_certificate_dry_run(self, mock_acme_crypto, mock_crypto): + csr = util.CSR(form="pem", file=None, data=CSR_SAN) + mock_acme_crypto.make_csr.return_value = CSR_SAN + mock_crypto.make_key.return_value = mock.sentinel.key_pem + key = util.Key(file=None, pem=mock.sentinel.key_pem) + + with mock.patch.object(self.client.config, 'dry_run', new=True): + self._test_obtain_certificate_common(key, csr) + + mock_crypto.make_key.assert_called_once_with(self.config.rsa_key_size) + mock_acme_crypto.make_csr.assert_called_once_with( + mock.sentinel.key_pem, self.eg_domains, self.config.must_staple) + mock_crypto.init_save_key.assert_not_called() + mock_crypto.init_save_csr.assert_not_called() + + def _test_obtain_certificate_common(self, key, csr): + self._mock_obtain_certificate() # return_value is essentially set to (None, None) in # _mock_obtain_certificate(), which breaks this test. @@ -258,7 +281,7 @@ class ClientTest(ClientTestCommon): authzr = [] # domain ordering should not be affected by authorization order - for domain in reversed(domains): + for domain in reversed(self.eg_domains): authzr.append( mock.MagicMock( body=mock.MagicMock( @@ -267,14 +290,12 @@ class ClientTest(ClientTestCommon): self.client.auth_handler.get_authorizations.return_value = authzr - self.assertEqual( - self.client.obtain_certificate(domains), - (mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr)) + with test_util.patch_get_utility(): + result = self.client.obtain_certificate(self.eg_domains) - mock_crypto_util.init_save_key.assert_called_once_with( - self.config.rsa_key_size, self.config.key_dir) - mock_crypto_util.init_save_csr.assert_called_once_with( - mock.sentinel.key, domains, self.config.csr_dir) + self.assertEqual( + result, + (mock.sentinel.certr, mock.sentinel.chain, key, csr)) self._check_obtain_certificate() @mock.patch('certbot.client.Client.obtain_certificate') diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index ea5a81062..729b09dc1 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -30,8 +30,7 @@ class InitSaveKeyTest(test_util.TempDirTestCase): logging.disable(logging.CRITICAL) zope.component.provideUtility( - mock.Mock(strict_permissions=True, dry_run=False), - interfaces.IConfig) + mock.Mock(strict_permissions=True), interfaces.IConfig) def tearDown(self): super(InitSaveKeyTest, self).tearDown() @@ -51,16 +50,6 @@ class InitSaveKeyTest(test_util.TempDirTestCase): self.assertTrue('key-certbot.pem' in key.file) self.assertTrue(os.path.exists(os.path.join(self.tempdir, key.file))) - @mock.patch('certbot.crypto_util.make_key') - def test_success_dry_run(self, mock_make): - zope.component.provideUtility( - mock.Mock(strict_permissions=True, dry_run=True), - interfaces.IConfig) - mock_make.return_value = b'key_pem' - key = self._call(1024, self.tempdir) - self.assertEqual(key.pem, b'key_pem') - self.assertTrue(key.file is None) - @mock.patch('certbot.crypto_util.make_key') def test_key_failure(self, mock_make): mock_make.side_effect = ValueError @@ -74,12 +63,11 @@ class InitSaveCSRTest(test_util.TempDirTestCase): super(InitSaveCSRTest, self).setUp() zope.component.provideUtility( - mock.Mock(strict_permissions=True, dry_run=False), - interfaces.IConfig) + mock.Mock(strict_permissions=True), interfaces.IConfig) @mock.patch('acme.crypto_util.make_csr') @mock.patch('certbot.crypto_util.util.make_or_verify_dir') - def test_success(self, unused_mock_verify, mock_csr): + def test_it(self, unused_mock_verify, mock_csr): from certbot.crypto_util import init_save_csr mock_csr.return_value = b'csr_pem' @@ -90,22 +78,6 @@ class InitSaveCSRTest(test_util.TempDirTestCase): self.assertEqual(csr.data, b'csr_pem') self.assertTrue('csr-certbot.pem' in csr.file) - @mock.patch('acme.crypto_util.make_csr') - @mock.patch('certbot.crypto_util.util.make_or_verify_dir') - def test_success_dry_run(self, unused_mock_verify, mock_csr): - from certbot.crypto_util import init_save_csr - - zope.component.provideUtility( - mock.Mock(strict_permissions=True, dry_run=True), - interfaces.IConfig) - mock_csr.return_value = b'csr_pem' - - csr = init_save_csr( - mock.Mock(pem='dummy_key'), 'example.com', self.tempdir) - - self.assertEqual(csr.data, b'csr_pem') - self.assertTrue(csr.file is None) - class ValidCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.valid_csr.""" From d57e8bfaa39f0b1286f82c8a5d33e2cbf46558fb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:11:51 -0400 Subject: [PATCH 077/125] add --deploy-hook --- certbot/cli.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index 14874e63e..42510ad11 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1094,6 +1094,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " and keys; the shell variable $RENEWED_DOMAINS will contain a" " space-delimited list of renewed certificate domains (for example," " \"example.com www.example.com\"") + helpful.add( + "renew", "--deploy-hook", + help="Command to be run in a shell once for each successfully" + " issued certificate. For this command, the shell variable" + " $RENEWED_LINEAGE will point to the config live subdirectory" + ' (for example, "/etc/letsencrypt/live/example.com") containing' + " the new certificates and keys; the shell variable" + " $RENEWED_DOMAINS will contain a space-delimited list of" + ' renewed certificate domains (for example, "example.com' + ' www.example.com"') helpful.add( "renew", "--disable-hook-validation", action='store_false', dest='validate_hooks', default=True, From af354e909916e72068e8824456fb28b0ed6d011c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:18:07 -0400 Subject: [PATCH 078/125] add --deploy-hook parsing --- certbot/cli.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index 42510ad11..5aa267414 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1095,7 +1095,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " space-delimited list of renewed certificate domains (for example," " \"example.com www.example.com\"") helpful.add( - "renew", "--deploy-hook", + "renew", "--deploy-hook", action=_DeployHookAction, help="Command to be run in a shell once for each successfully" " issued certificate. For this command, the shell variable" " $RENEWED_LINEAGE will point to the config live subdirectory" @@ -1359,3 +1359,14 @@ def parse_preferred_challenges(pref_challs): raise errors.Error( "Unrecognized challenges: {0}".format(unrecognized)) return challs + + +class _DeployHookAction(argparse.Action): + """Action class for parsing deploy hooks.""" + + def __call__(self, parser, namespace, values, option_string=None): + renew_hook_set = namespace.deploy_hook != namespace.renew_hook + if renew_hook_set and namespace.renew_hook != values: + raise argparse.ArgumentError( + self, "conflicts with --renew-hook value") + namespace.deploy_hook = namespace.renew_hook = values From 5cf82e4843038ecb86aeb01bf1d298f65ac9d5eb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:26:18 -0400 Subject: [PATCH 079/125] test --deploy-hook --- 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 f18da240a..84463d2d7 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -323,6 +323,18 @@ class ParseTest(unittest.TestCase): self.assertRaises( errors.Error, self.parse, "-n --force-interactive".split()) + def test_deploy_hook_conflict(self): + with mock.patch("certbot.cli.sys.stderr"): + self.assertRaises(SystemExit, self.parse, + "--renew-hook foo --deploy-hook bar".split()) + + def test_deploy_hook_sets_renew_hook(self): + value = "foo" + namespace = self.parse( + ["--deploy-hook", value, "--disable-hook-validation"]) + self.assertEqual(namespace.deploy_hook, value) + self.assertEqual(namespace.renew_hook, value) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" From ed4be4117cbb9146d9754bc38e8d8eb54a1a0980 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:29:39 -0400 Subject: [PATCH 080/125] hide --renew-hook --- certbot/cli.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 5aa267414..8e0085390 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1085,15 +1085,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " run if an attempt was made to obtain/renew a certificate. If" " multiple renewed certificates have identical post-hooks, only" " one will be run.") - 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 (for example," - " \"/etc/letsencrypt/live/example.com\") containing the new certificates" - " and keys; the shell variable $RENEWED_DOMAINS will contain a" - " space-delimited list of renewed certificate domains (for example," - " \"example.com www.example.com\"") + helpful.add("renew", "--renew-hook", help=argparse.SUPPRESS) helpful.add( "renew", "--deploy-hook", action=_DeployHookAction, help="Command to be run in a shell once for each successfully" From 220d486190f982f60efd90b30ddd492305a18b4f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:36:46 -0400 Subject: [PATCH 081/125] remove --renew-hook from help output --- certbot/cli.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 8e0085390..8e078c058 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -887,7 +887,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " in order to obtain test certificates, and reloads webservers to deploy and then" " roll back those changes. It also calls --pre-hook and --post-hook commands" " if they are defined because they may be necessary to accurately simulate" - " renewal. --renew-hook commands are not called.") + " renewal. --deploy-hook commands are not called.") helpful.add( ["register", "automation"], "--register-unsafely-without-email", action="store_true", help="Specifying this flag enables registering an account with no " @@ -1100,11 +1100,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "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" - " shell constructs, so you can use this switch to disable it." + " --pre-hook/--post-hook/--deploy-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." " (default: False)") helpful.add_deprecated_argument("--agree-dev-preview", 0) From 4243db1525c569f5cf55e372a9714a7e6a0121e8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:38:39 -0400 Subject: [PATCH 082/125] test --renew-hook is hidden --- 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 84463d2d7..e76db02f7 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -109,6 +109,7 @@ class ParseTest(unittest.TestCase): self.assertTrue("--dialog" not in out) self.assertTrue("%s" not in out) self.assertTrue("{0}" not in out) + self.assertTrue("--renew-hook" not in out) out = self._help_output(['-h', 'nginx']) if "nginx" in PLUGINS: From feffeb275b3123ccba8663b1a37795daf6fccf42 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:46:32 -0400 Subject: [PATCH 083/125] add --renew-hook error handling --- certbot/cli.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index 8e078c058..fd9378079 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1085,7 +1085,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " run if an attempt was made to obtain/renew a certificate. If" " multiple renewed certificates have identical post-hooks, only" " one will be run.") - helpful.add("renew", "--renew-hook", help=argparse.SUPPRESS) + helpful.add("renew", "--renew-hook", + action=_RenewHookAction, help=argparse.SUPPRESS) helpful.add( "renew", "--deploy-hook", action=_DeployHookAction, help="Command to be run in a shell once for each successfully" @@ -1363,3 +1364,14 @@ class _DeployHookAction(argparse.Action): raise argparse.ArgumentError( self, "conflicts with --renew-hook value") namespace.deploy_hook = namespace.renew_hook = values + + +class _RenewHookAction(argparse.Action): + """Action class for parsing renew hooks.""" + + def __call__(self, parser, namespace, values, option_string=None): + deploy_hook_set = namespace.deploy_hook is not None + if deploy_hook_set and namespace.deploy_hook != values: + raise argparse.ArgumentError( + self, "conflicts with --deploy-hook value") + namespace.renew_hook = values From ad4ed22932740d1c178acae485a9a9f1a9c59cc4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:55:36 -0400 Subject: [PATCH 084/125] test --renew-hook --- certbot/tests/cli_test.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index e76db02f7..3a943fd1a 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -40,7 +40,7 @@ class TestReadFile(TempDirTestCase): -class ParseTest(unittest.TestCase): +class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods '''Test the cli args entrypoint''' _multiprocess_can_split_ = True @@ -336,6 +336,18 @@ class ParseTest(unittest.TestCase): self.assertEqual(namespace.deploy_hook, value) self.assertEqual(namespace.renew_hook, value) + def test_renew_hook_conflict(self): + with mock.patch("certbot.cli.sys.stderr"): + self.assertRaises(SystemExit, self.parse, + "--deploy-hook foo --renew-hook bar".split()) + + def test_renew_hook_does_not_set_renew_hook(self): + value = "foo" + namespace = self.parse( + ["--renew-hook", value, "--disable-hook-validation"]) + self.assertEqual(namespace.deploy_hook, None) + self.assertEqual(namespace.renew_hook, value) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" From 32fa3b1d04cac71f90b5ac6d9bf92828b37c1306 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 09:59:19 -0400 Subject: [PATCH 085/125] test deploy-hook and renew-hook match --- certbot/tests/cli_test.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 3a943fd1a..cad04f88e 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -329,6 +329,14 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertRaises(SystemExit, self.parse, "--renew-hook foo --deploy-hook bar".split()) + def test_deploy_hook_matches_renew_hook(self): + value = "foo" + namespace = self.parse(["--renew-hook", value, + "--deploy-hook", value, + "--disable-hook-validation"]) + self.assertEqual(namespace.deploy_hook, value) + self.assertEqual(namespace.renew_hook, value) + def test_deploy_hook_sets_renew_hook(self): value = "foo" namespace = self.parse( @@ -341,6 +349,14 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertRaises(SystemExit, self.parse, "--deploy-hook foo --renew-hook bar".split()) + def test_renew_hook_matches_deploy_hook(self): + value = "foo" + namespace = self.parse(["--deploy-hook", value, + "--renew-hook", value, + "--disable-hook-validation"]) + self.assertEqual(namespace.deploy_hook, value) + self.assertEqual(namespace.renew_hook, value) + def test_renew_hook_does_not_set_renew_hook(self): value = "foo" namespace = self.parse( From e94ee31a6f556fc32e424dd7562b6ea363e21bea Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 10:24:00 -0400 Subject: [PATCH 086/125] add hooks.deploy_hook --- certbot/hooks.py | 19 +++++++++++++++++-- certbot/tests/hook_test.py | 3 ++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/certbot/hooks.py b/certbot/hooks.py index b3c1fc3e2..799ef90b7 100644 --- a/certbot/hooks.py +++ b/certbot/hooks.py @@ -18,6 +18,7 @@ def validate_hooks(config): """Check hook commands are executable.""" validate_hook(config.pre_hook, "pre") validate_hook(config.post_hook, "post") + validate_hook(config.deploy_hook, "deploy") validate_hook(config.renew_hook, "renew") @@ -95,16 +96,30 @@ def run_saved_post_hooks(): _run_hook(cmd) +def deploy_hook(config, domains, lineage_path): + """Run post-issuance hook if defined. + + :param configuration.NamespaceConfig config: Certbot settings + :param domains: domains in the obtained certificate + :type domains: `list` of `str` + :param str lineage_path: live directory path for the new cert + + """ + if config.deploy_hook: + renew_hook(config, domains, lineage_path) + + def renew_hook(config, domains, lineage_path): """Run post-renewal hook if defined.""" if config.renew_hook: if not config.dry_run: os.environ["RENEWED_DOMAINS"] = " ".join(domains) os.environ["RENEWED_LINEAGE"] = lineage_path - logger.info("Running renew-hook command: %s", config.renew_hook) + logger.info("Running deploy-hook command: %s", config.renew_hook) _run_hook(config.renew_hook) else: - logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook) + logger.warning( + "Dry run: skipping deploy hook command: %s", config.renew_hook) def _run_hook(shell_cmd): diff --git a/certbot/tests/hook_test.py b/certbot/tests/hook_test.py index 0fbb91492..fe2579a89 100644 --- a/certbot/tests/hook_test.py +++ b/certbot/tests/hook_test.py @@ -16,7 +16,8 @@ class HookTest(unittest.TestCase): @mock.patch('certbot.hooks._prog') def test_validate_hooks(self, mock_prog): - config = mock.MagicMock(pre_hook="", post_hook="ls -lR", renew_hook="uptime") + config = mock.MagicMock(deploy_hook=None, pre_hook="", + post_hook="ls -lR", renew_hook="uptime") hooks.validate_hooks(config) self.assertEqual(mock_prog.call_count, 2) self.assertEqual(mock_prog.call_args_list[1][0][0], 'uptime') From 1b65ba88d8f86a96b1e231ae657e39e69588c631 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 10:30:33 -0400 Subject: [PATCH 087/125] test hooks.deploy_hook --- certbot/tests/hook_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/certbot/tests/hook_test.py b/certbot/tests/hook_test.py index fe2579a89..28417ac73 100644 --- a/certbot/tests/hook_test.py +++ b/certbot/tests/hook_test.py @@ -36,6 +36,19 @@ class HookTest(unittest.TestCase): self.assertEqual(hooks._prog("funky"), None) self.assertEqual(mock_ps.call_count, 1) + @mock.patch('certbot.hooks.renew_hook') + def test_deploy_hook(self, mock_renew_hook): + args = (mock.Mock(deploy_hook='foo'), ['example.org'], 'path',) + # pylint: disable=star-args + hooks.deploy_hook(*args) + mock_renew_hook.assert_called_once_with(*args) + + @mock.patch('certbot.hooks.renew_hook') + def test_no_deploy_hook(self, mock_renew_hook): + args = (mock.Mock(deploy_hook=None), ['example.org'], 'path',) + hooks.deploy_hook(*args) # pylint: disable=star-args + mock_renew_hook.assert_not_called() + def _test_a_hook(self, config, hook_function, calls_expected, **kwargs): with mock.patch('certbot.hooks.logger') as mock_logger: mock_logger.warning = mock.MagicMock() From 8a664622eac4ca2fa23edf065228ed0ff967f6bd Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 10:33:49 -0400 Subject: [PATCH 088/125] Call deploy_hook during certonly and run --- certbot/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot/main.py b/certbot/main.py index cd87706b4..f7421d75e 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -82,6 +82,8 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N lineage = le_client.obtain_and_enroll_certificate(domains, certname) if lineage is False: raise errors.Error("Certificate could not be obtained") + elif lineage is not None: + hooks.deploy_hook(config, lineage.names(), lineage.live_dir) finally: hooks.post_hook(config) From 6dedfa62b63d7d20b60461eeea3d32513f49d0f6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 11:06:51 -0400 Subject: [PATCH 089/125] Test renew and deploy hooks are run properly --- tests/boulder-integration.sh | 52 +++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 913880c8b..a356da884 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -52,15 +52,19 @@ CheckHooks() { if [ $(head -n1 $HOOK_TEST) = "wtf.pre" ]; then echo "wtf.pre" > "$EXPECTED" echo "wtf2.pre" >> "$EXPECTED" - echo "renew" >> "$EXPECTED" - echo "renew" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" echo "wtf.post" >> "$EXPECTED" echo "wtf2.post" >> "$EXPECTED" else echo "wtf2.pre" > "$EXPECTED" echo "wtf.pre" >> "$EXPECTED" - echo "renew" >> "$EXPECTED" - echo "renew" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" + echo "deploy" >> "$EXPECTED" echo "wtf2.post" >> "$EXPECTED" echo "wtf.post" >> "$EXPECTED" fi @@ -74,6 +78,27 @@ CheckHooks() { rm "$HOOK_TEST" } +# Checks if deploy was run and deletes the hook file +CheckDeployHook() { + CONTENTS=$(cat "$HOOK_TEST") + rm "$HOOK_TEST" + grep deploy <(echo "$CONTENTS") +} + +# Asserts the deploy hook was run and deletes the hook file +AssertDeployHook() { + if ! CheckDeployHook; then + echo "The deploy hook wasn't run" >&2 + fi +} + +# Asserts the deploy hook wasn't run and deletes the hook file +AssertNoDeployHook() { + if CheckDeployHook; then + echo "The deploy hook was incorrectly run" >&2 + fi +} + # Cleanup coverage data coverage erase @@ -104,24 +129,33 @@ python_server_pid=$! common --domains le1.wtf --preferred-challenges tls-sni-01 auth \ --pre-hook 'echo wtf.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf.post >> "$HOOK_TEST"'\ - --renew-hook 'echo renew >> "$HOOK_TEST"' + --deploy-hook 'echo deploy >> "$HOOK_TEST"' kill $python_server_pid +AssertDeployHook + python ./tests/run_http_server.py $tls_sni_01_port & python_server_pid=$! common --domains le2.wtf --preferred-challenges http-01 run \ --pre-hook 'echo wtf.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf.post >> "$HOOK_TEST"'\ - --renew-hook 'echo renew >> "$HOOK_TEST"' + --deploy-hook 'echo deploy >> "$HOOK_TEST"' kill $python_server_pid +AssertDeployHook 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 \ --pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \ - --post-hook 'echo wtf2.post >> "$HOOK_TEST"' + --post-hook 'echo wtf2.post >> "$HOOK_TEST"' \ + --renew-hook 'echo deploy >> "$HOOK_TEST"' +AssertNoDeployHook -common certonly -a manual -d dns.le.wtf --preferred-challenges dns,tls-sni \ - --manual-auth-hook ./tests/manual-dns-auth.sh +common -a manual -d dns.le.wtf --preferred-challenges dns,tls-sni run \ + --manual-auth-hook ./tests/manual-dns-auth.sh \ + --pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \ + --post-hook 'echo wtf2.post >> "$HOOK_TEST"' \ + --renew-hook 'echo deploy >> "$HOOK_TEST"' +AssertNoDeployHook common certonly --cert-name newname -d newname.le.wtf From 4c19d19cf5b3fcecd5ca6a0c1b3d68c254efe379 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 11:30:21 -0400 Subject: [PATCH 090/125] Test that deploy and renew hooks are saved right It is important that both renew and deploy hooks are saved as renew_hook in renewal configuration files to preserve forwards compatibility. --- tests/boulder-integration.sh | 59 +++++++++++++++++++++++++++--------- tests/integration/_common.sh | 5 +-- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index a356da884..181dcc5fb 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -78,25 +78,47 @@ CheckHooks() { rm "$HOOK_TEST" } -# Checks if deploy was run and deletes the hook file -CheckDeployHook() { +# Checks if deploy is in the hook output and deletes the file +DeployInHookOutput() { CONTENTS=$(cat "$HOOK_TEST") rm "$HOOK_TEST" grep deploy <(echo "$CONTENTS") } -# Asserts the deploy hook was run and deletes the hook file -AssertDeployHook() { - if ! CheckDeployHook; then - echo "The deploy hook wasn't run" >&2 +# Asserts that there is a saved renew_hook for a lineage. +# +# Arguments: +# Name of lineage to check +CheckSavedRenewHook() { + if ! grep renew_hook "$config_dir/renewal/$1.conf"; then + echo "Hook wasn't saved as renew_hook" >&2 + exit 1 fi } -# Asserts the deploy hook wasn't run and deletes the hook file -AssertNoDeployHook() { - if CheckDeployHook; then - echo "The deploy hook was incorrectly run" >&2 +# Asserts the deploy hook was properly run and saved and deletes the hook file +# +# Arguments: +# Lineage name of the issued cert +CheckDeployHook() { + if ! DeployInHookOutput; then + echo "The deploy hook wasn't run" >&2 + exit 1 fi + CheckSavedRenewHook $1 +} + +# Asserts the renew hook wasn't run but was saved and deletes the hook file +# +# Arguments: +# Lineage name of the issued cert +# Asserts the deploy hook wasn't run and deletes the hook file +CheckRenewHook() { + if DeployInHookOutput; then + echo "The renew hook was incorrectly run" >&2 + exit 1 + fi + CheckSavedRenewHook $1 } # Cleanup coverage data @@ -126,36 +148,43 @@ common plugins --init --prepare | grep webroot python ./tests/run_http_server.py $http_01_port & python_server_pid=$! +certname="le1.wtf" common --domains le1.wtf --preferred-challenges tls-sni-01 auth \ + --cert-name $certname \ --pre-hook 'echo wtf.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf.post >> "$HOOK_TEST"'\ --deploy-hook 'echo deploy >> "$HOOK_TEST"' kill $python_server_pid -AssertDeployHook +CheckDeployHook $certname python ./tests/run_http_server.py $tls_sni_01_port & python_server_pid=$! +certname="le2.wtf" common --domains le2.wtf --preferred-challenges http-01 run \ + --cert-name $certname \ --pre-hook 'echo wtf.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf.post >> "$HOOK_TEST"'\ --deploy-hook 'echo deploy >> "$HOOK_TEST"' kill $python_server_pid -AssertDeployHook +CheckDeployHook $certname -common certonly -a manual -d le.wtf --rsa-key-size 4096 \ +certname="le.wtf" +common certonly -a manual -d le.wtf --rsa-key-size 4096 --cert-name $certname \ --manual-auth-hook ./tests/manual-http-auth.sh \ --manual-cleanup-hook ./tests/manual-http-cleanup.sh \ --pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf2.post >> "$HOOK_TEST"' \ --renew-hook 'echo deploy >> "$HOOK_TEST"' -AssertNoDeployHook +CheckRenewHook $certname +certname="dns.le.wtf" common -a manual -d dns.le.wtf --preferred-challenges dns,tls-sni run \ + --cert-name $certname \ --manual-auth-hook ./tests/manual-dns-auth.sh \ --pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf2.post >> "$HOOK_TEST"' \ --renew-hook 'echo deploy >> "$HOOK_TEST"' -AssertNoDeployHook +CheckRenewHook $certname common certonly --cert-name newname -d newname.le.wtf diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 48d20eb3b..d151bdc3f 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -2,12 +2,13 @@ # the kernel to use. root=${root:-$(mktemp -d -t leitXXXX)} echo "Root integration tests directory: $root" -store_flags="--config-dir $root/conf --work-dir $root/work" +config_dir="$root/conf" +store_flags="--config-dir $config_dir --work-dir $root/work" store_flags="$store_flags --logs-dir $root/logs" tls_sni_01_port=5001 http_01_port=5002 sources="acme/,$(ls -dm certbot*/ | tr -d ' \n')" -export root store_flags tls_sni_01_port http_01_port sources +export root config_dir store_flags tls_sni_01_port http_01_port sources certbot_test () { certbot_test_no_force_renew \ From 62327b49c37b9c9114ae2daac14f9b41513fa590 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Jun 2017 11:40:34 -0400 Subject: [PATCH 091/125] Test hook validation order --- certbot/tests/hook_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/certbot/tests/hook_test.py b/certbot/tests/hook_test.py index 28417ac73..964d69866 100644 --- a/certbot/tests/hook_test.py +++ b/certbot/tests/hook_test.py @@ -26,6 +26,19 @@ class HookTest(unittest.TestCase): config = mock.MagicMock(pre_hook="explodinator", post_hook="", renew_hook="") self.assertRaises(errors.HookCommandNotFound, hooks.validate_hooks, config) + @mock.patch('certbot.hooks.validate_hook') + def test_validation_order(self, mock_validate_hook): + # This ensures error messages are about deploy hook when appropriate + config = mock.Mock(deploy_hook=None, pre_hook=None, + post_hook=None, renew_hook=None) + hooks.validate_hooks(config) + + order = [call[0][1] for call in mock_validate_hook.call_args_list] + self.assertTrue('pre' in order) + self.assertTrue('post' in order) + self.assertTrue('deploy' in order) + self.assertEqual(order[-1], 'renew') + @mock.patch('certbot.hooks.util.exe_exists') @mock.patch('certbot.hooks.plug_util.path_surgery') def test_prog(self, mock_ps, mock_exe_exists): From 97b22da1b6c58861376648bbad8a756304356adf Mon Sep 17 00:00:00 2001 From: ohemorange Date: Fri, 30 Jun 2017 17:12:09 -0700 Subject: [PATCH 092/125] Replace the easy v. secure prompt with more clear choices (#4897) * Replace the easy v. secure prompt with more clear choices --- certbot/display/enhancements.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/certbot/display/enhancements.py b/certbot/display/enhancements.py index d2ffe2e0d..0f6b6c57d 100644 --- a/certbot/display/enhancements.py +++ b/certbot/display/enhancements.py @@ -42,12 +42,14 @@ def redirect_by_default(): """ choices = [ - ("Easy", "Allow both HTTP and HTTPS access to these sites"), - ("Secure", "Make all requests redirect to secure HTTPS access"), + ("No redirect", "Make no further changes to the webserver configuration."), + ("Redirect", "Make all requests redirect to secure HTTPS access. " + "Choose this for new sites, or if you're confident your site works on HTTPS. " + "You can undo this change by editing your web server's configuration."), ] code, selection = util(interfaces.IDisplay).menu( - "Please choose whether HTTPS access is required or optional.", + "Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.", choices, default=0, cli_flag="--redirect / --no-redirect", force_interactive=True) From ec35828b9addc105c01c88d0dcb5b7b9d1279d73 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Sun, 2 Jul 2017 00:12:16 +0200 Subject: [PATCH 093/125] Added missing rst files after doing an inspection of the file structure --- docs/api/cert_manager.rst | 5 +++++ docs/api/cli.rst | 5 +++++ docs/api/eff.rst | 5 +++++ docs/api/error_handler.rst | 5 +++++ docs/api/hooks.rst | 5 +++++ docs/api/lock.rst | 5 +++++ docs/api/log.rst | 5 +++++ docs/api/main.rst | 5 +++++ docs/api/notify.rst | 5 +++++ docs/api/ocsp.rst | 5 +++++ docs/api/plugins/common_test.rst | 5 +++++ docs/api/plugins/disco_test.rst | 5 +++++ docs/api/plugins/dns_common_lexicon_test.rst | 5 +++++ docs/api/plugins/dns_common_test.rst | 5 +++++ docs/api/plugins/dns_test_common.rst | 5 +++++ docs/api/plugins/dns_test_common_lexicon.rst | 5 +++++ docs/api/plugins/manual_test.rst | 5 +++++ docs/api/plugins/null.rst | 5 +++++ docs/api/plugins/null_test.rst | 5 +++++ docs/api/plugins/selection.rst | 5 +++++ docs/api/plugins/selection_test.rst | 5 +++++ docs/api/plugins/standalone_test.rst | 5 +++++ docs/api/plugins/webroot_test.rst | 5 +++++ docs/api/renewal.rst | 5 +++++ 24 files changed, 120 insertions(+) create mode 100644 docs/api/cert_manager.rst create mode 100644 docs/api/cli.rst create mode 100644 docs/api/eff.rst create mode 100644 docs/api/error_handler.rst create mode 100644 docs/api/hooks.rst create mode 100644 docs/api/lock.rst create mode 100644 docs/api/log.rst create mode 100644 docs/api/main.rst create mode 100644 docs/api/notify.rst create mode 100644 docs/api/ocsp.rst create mode 100644 docs/api/plugins/common_test.rst create mode 100644 docs/api/plugins/disco_test.rst create mode 100644 docs/api/plugins/dns_common_lexicon_test.rst create mode 100644 docs/api/plugins/dns_common_test.rst create mode 100644 docs/api/plugins/dns_test_common.rst create mode 100644 docs/api/plugins/dns_test_common_lexicon.rst create mode 100644 docs/api/plugins/manual_test.rst create mode 100644 docs/api/plugins/null.rst create mode 100644 docs/api/plugins/null_test.rst create mode 100644 docs/api/plugins/selection.rst create mode 100644 docs/api/plugins/selection_test.rst create mode 100644 docs/api/plugins/standalone_test.rst create mode 100644 docs/api/plugins/webroot_test.rst create mode 100644 docs/api/renewal.rst diff --git a/docs/api/cert_manager.rst b/docs/api/cert_manager.rst new file mode 100644 index 000000000..c8c86f8b9 --- /dev/null +++ b/docs/api/cert_manager.rst @@ -0,0 +1,5 @@ +:mod:`certbot.cert_manager` +------------------------------- + +.. automodule:: certbot.cert_manager + :members: diff --git a/docs/api/cli.rst b/docs/api/cli.rst new file mode 100644 index 000000000..91be86758 --- /dev/null +++ b/docs/api/cli.rst @@ -0,0 +1,5 @@ +:mod:`certbot.cli` +---------------------- + +.. automodule:: certbot.cli + :members: diff --git a/docs/api/eff.rst b/docs/api/eff.rst new file mode 100644 index 000000000..2924b256d --- /dev/null +++ b/docs/api/eff.rst @@ -0,0 +1,5 @@ +:mod:`certbot.eff` +---------------------- + +.. automodule:: certbot.eff + :members: diff --git a/docs/api/error_handler.rst b/docs/api/error_handler.rst new file mode 100644 index 000000000..f1306177d --- /dev/null +++ b/docs/api/error_handler.rst @@ -0,0 +1,5 @@ +:mod:`certbot.error_handler` +-------------------------------- + +.. automodule:: certbot.error_handler + :members: diff --git a/docs/api/hooks.rst b/docs/api/hooks.rst new file mode 100644 index 000000000..140fbf284 --- /dev/null +++ b/docs/api/hooks.rst @@ -0,0 +1,5 @@ +:mod:`certbot.hooks` +------------------------ + +.. automodule:: certbot.hooks + :members: diff --git a/docs/api/lock.rst b/docs/api/lock.rst new file mode 100644 index 000000000..6dcbf9589 --- /dev/null +++ b/docs/api/lock.rst @@ -0,0 +1,5 @@ +:mod:`certbot.lock` +----------------------- + +.. automodule:: certbot.lock + :members: diff --git a/docs/api/log.rst b/docs/api/log.rst new file mode 100644 index 000000000..0b1cbd09c --- /dev/null +++ b/docs/api/log.rst @@ -0,0 +1,5 @@ +:mod:`certbot.log` +---------------------- + +.. automodule:: log.eff + :members: diff --git a/docs/api/main.rst b/docs/api/main.rst new file mode 100644 index 000000000..a555bab01 --- /dev/null +++ b/docs/api/main.rst @@ -0,0 +1,5 @@ +:mod:`certbot.main` +----------------------- + +.. automodule:: certbot.main + :members: diff --git a/docs/api/notify.rst b/docs/api/notify.rst new file mode 100644 index 000000000..fa042b2d2 --- /dev/null +++ b/docs/api/notify.rst @@ -0,0 +1,5 @@ +:mod:`certbot.notify` +------------------------- + +.. automodule:: certbot.notify + :members: diff --git a/docs/api/ocsp.rst b/docs/api/ocsp.rst new file mode 100644 index 000000000..7044f4052 --- /dev/null +++ b/docs/api/ocsp.rst @@ -0,0 +1,5 @@ +:mod:`certbot.ocsp` +----------------------- + +.. automodule:: certbot.ocsp + :members: diff --git a/docs/api/plugins/common_test.rst b/docs/api/plugins/common_test.rst new file mode 100644 index 000000000..f01e78e7b --- /dev/null +++ b/docs/api/plugins/common_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.common_test` +-------------------------------------- + +.. automodule:: certbot.plugins.common_test + :members: diff --git a/docs/api/plugins/disco_test.rst b/docs/api/plugins/disco_test.rst new file mode 100644 index 000000000..6b9b73c6b --- /dev/null +++ b/docs/api/plugins/disco_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.disco_test` +------------------------------------- + +.. automodule:: certbot.plugins.disco_test + :members: diff --git a/docs/api/plugins/dns_common_lexicon_test.rst b/docs/api/plugins/dns_common_lexicon_test.rst new file mode 100644 index 000000000..22ca71c66 --- /dev/null +++ b/docs/api/plugins/dns_common_lexicon_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.dns_common_lexicon_test` +-------------------------------------------------- + +.. automodule:: certbot.plugins.dns_common_lexicon_test + :members: diff --git a/docs/api/plugins/dns_common_test.rst b/docs/api/plugins/dns_common_test.rst new file mode 100644 index 000000000..c7c82fe14 --- /dev/null +++ b/docs/api/plugins/dns_common_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.dns_common_test` +------------------------------------------ + +.. automodule:: certbot.plugins.dns_common_test + :members: diff --git a/docs/api/plugins/dns_test_common.rst b/docs/api/plugins/dns_test_common.rst new file mode 100644 index 000000000..a02226a25 --- /dev/null +++ b/docs/api/plugins/dns_test_common.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.dns_test_common` +------------------------------------------ + +.. automodule:: certbot.plugins.dns_test_common + :members: diff --git a/docs/api/plugins/dns_test_common_lexicon.rst b/docs/api/plugins/dns_test_common_lexicon.rst new file mode 100644 index 000000000..e58d56cf2 --- /dev/null +++ b/docs/api/plugins/dns_test_common_lexicon.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.dns_test_common_lexicon` +-------------------------------------------------- + +.. automodule:: certbot.plugins.dns_test_common_lexicon + :members: diff --git a/docs/api/plugins/manual_test.rst b/docs/api/plugins/manual_test.rst new file mode 100644 index 000000000..b9d405d00 --- /dev/null +++ b/docs/api/plugins/manual_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.manual_test` +-------------------------------------- + +.. automodule:: certbot.plugins.manual_test + :members: diff --git a/docs/api/plugins/null.rst b/docs/api/plugins/null.rst new file mode 100644 index 000000000..97df40987 --- /dev/null +++ b/docs/api/plugins/null.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.null` +------------------------------- + +.. automodule:: certbot.plugins.null + :members: diff --git a/docs/api/plugins/null_test.rst b/docs/api/plugins/null_test.rst new file mode 100644 index 000000000..4a8e1efb3 --- /dev/null +++ b/docs/api/plugins/null_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.null_test` +------------------------------------ + +.. automodule:: certbot.plugins.null_test + :members: diff --git a/docs/api/plugins/selection.rst b/docs/api/plugins/selection.rst new file mode 100644 index 000000000..6211bf9c0 --- /dev/null +++ b/docs/api/plugins/selection.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.selection` +------------------------------------ + +.. automodule:: certbot.plugins.selection + :members: diff --git a/docs/api/plugins/selection_test.rst b/docs/api/plugins/selection_test.rst new file mode 100644 index 000000000..59c380c60 --- /dev/null +++ b/docs/api/plugins/selection_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.selection_test` +----------------------------------------- + +.. automodule:: certbot.plugins.selection_test + :members: diff --git a/docs/api/plugins/standalone_test.rst b/docs/api/plugins/standalone_test.rst new file mode 100644 index 000000000..21fd8eff5 --- /dev/null +++ b/docs/api/plugins/standalone_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.standalone_test` +------------------------------------------ + +.. automodule:: certbot.plugins.standalone_test + :members: diff --git a/docs/api/plugins/webroot_test.rst b/docs/api/plugins/webroot_test.rst new file mode 100644 index 000000000..893ea3067 --- /dev/null +++ b/docs/api/plugins/webroot_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.webroot_test` +--------------------------------------- + +.. automodule:: certbot.plugins.webroot_test + :members: diff --git a/docs/api/renewal.rst b/docs/api/renewal.rst new file mode 100644 index 000000000..58557351f --- /dev/null +++ b/docs/api/renewal.rst @@ -0,0 +1,5 @@ +:mod:`certbot.renewal` +-------------------------- + +.. automodule:: certbot.renewal + :members: From d118acf5248d95709ccb390a6a7ecf781e099857 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Sun, 2 Jul 2017 00:16:15 +0200 Subject: [PATCH 094/125] Correction to module name --- docs/api/log.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/log.rst b/docs/api/log.rst index 0b1cbd09c..41311de90 100644 --- a/docs/api/log.rst +++ b/docs/api/log.rst @@ -1,5 +1,5 @@ :mod:`certbot.log` ---------------------- -.. automodule:: log.eff +.. automodule:: certbot.log :members: From 054873034c04ee3730a396f21523f5e125408ef4 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Sun, 2 Jul 2017 00:31:58 +0200 Subject: [PATCH 095/125] Added missed rst file --- docs/api/plugins/util_test.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/api/plugins/util_test.rst diff --git a/docs/api/plugins/util_test.rst b/docs/api/plugins/util_test.rst new file mode 100644 index 000000000..cce425a38 --- /dev/null +++ b/docs/api/plugins/util_test.rst @@ -0,0 +1,5 @@ +:mod:`certbot.plugins.util_test` +------------------------------------ + +.. automodule:: certbot.plugins.util_test + :members: From 72c480ef18d73bbb4e8a99e71d4d2415dd4b3960 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Tue, 4 Jul 2017 23:52:24 +0200 Subject: [PATCH 096/125] Removed files with test in name after review comment from @ynasser --- docs/api/plugins/common_test.rst | 5 ----- docs/api/plugins/disco_test.rst | 5 ----- docs/api/plugins/dns_common_lexicon_test.rst | 5 ----- docs/api/plugins/dns_common_test.rst | 5 ----- docs/api/plugins/dns_test_common.rst | 5 ----- docs/api/plugins/dns_test_common_lexicon.rst | 5 ----- docs/api/plugins/manual_test.rst | 5 ----- docs/api/plugins/null_test.rst | 5 ----- docs/api/plugins/selection_test.rst | 5 ----- docs/api/plugins/standalone_test.rst | 5 ----- docs/api/plugins/util_test.rst | 5 ----- docs/api/plugins/webroot_test.rst | 5 ----- 12 files changed, 60 deletions(-) delete mode 100644 docs/api/plugins/common_test.rst delete mode 100644 docs/api/plugins/disco_test.rst delete mode 100644 docs/api/plugins/dns_common_lexicon_test.rst delete mode 100644 docs/api/plugins/dns_common_test.rst delete mode 100644 docs/api/plugins/dns_test_common.rst delete mode 100644 docs/api/plugins/dns_test_common_lexicon.rst delete mode 100644 docs/api/plugins/manual_test.rst delete mode 100644 docs/api/plugins/null_test.rst delete mode 100644 docs/api/plugins/selection_test.rst delete mode 100644 docs/api/plugins/standalone_test.rst delete mode 100644 docs/api/plugins/util_test.rst delete mode 100644 docs/api/plugins/webroot_test.rst diff --git a/docs/api/plugins/common_test.rst b/docs/api/plugins/common_test.rst deleted file mode 100644 index f01e78e7b..000000000 --- a/docs/api/plugins/common_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.common_test` --------------------------------------- - -.. automodule:: certbot.plugins.common_test - :members: diff --git a/docs/api/plugins/disco_test.rst b/docs/api/plugins/disco_test.rst deleted file mode 100644 index 6b9b73c6b..000000000 --- a/docs/api/plugins/disco_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.disco_test` -------------------------------------- - -.. automodule:: certbot.plugins.disco_test - :members: diff --git a/docs/api/plugins/dns_common_lexicon_test.rst b/docs/api/plugins/dns_common_lexicon_test.rst deleted file mode 100644 index 22ca71c66..000000000 --- a/docs/api/plugins/dns_common_lexicon_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.dns_common_lexicon_test` --------------------------------------------------- - -.. automodule:: certbot.plugins.dns_common_lexicon_test - :members: diff --git a/docs/api/plugins/dns_common_test.rst b/docs/api/plugins/dns_common_test.rst deleted file mode 100644 index c7c82fe14..000000000 --- a/docs/api/plugins/dns_common_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.dns_common_test` ------------------------------------------- - -.. automodule:: certbot.plugins.dns_common_test - :members: diff --git a/docs/api/plugins/dns_test_common.rst b/docs/api/plugins/dns_test_common.rst deleted file mode 100644 index a02226a25..000000000 --- a/docs/api/plugins/dns_test_common.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.dns_test_common` ------------------------------------------- - -.. automodule:: certbot.plugins.dns_test_common - :members: diff --git a/docs/api/plugins/dns_test_common_lexicon.rst b/docs/api/plugins/dns_test_common_lexicon.rst deleted file mode 100644 index e58d56cf2..000000000 --- a/docs/api/plugins/dns_test_common_lexicon.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.dns_test_common_lexicon` --------------------------------------------------- - -.. automodule:: certbot.plugins.dns_test_common_lexicon - :members: diff --git a/docs/api/plugins/manual_test.rst b/docs/api/plugins/manual_test.rst deleted file mode 100644 index b9d405d00..000000000 --- a/docs/api/plugins/manual_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.manual_test` --------------------------------------- - -.. automodule:: certbot.plugins.manual_test - :members: diff --git a/docs/api/plugins/null_test.rst b/docs/api/plugins/null_test.rst deleted file mode 100644 index 4a8e1efb3..000000000 --- a/docs/api/plugins/null_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.null_test` ------------------------------------- - -.. automodule:: certbot.plugins.null_test - :members: diff --git a/docs/api/plugins/selection_test.rst b/docs/api/plugins/selection_test.rst deleted file mode 100644 index 59c380c60..000000000 --- a/docs/api/plugins/selection_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.selection_test` ------------------------------------------ - -.. automodule:: certbot.plugins.selection_test - :members: diff --git a/docs/api/plugins/standalone_test.rst b/docs/api/plugins/standalone_test.rst deleted file mode 100644 index 21fd8eff5..000000000 --- a/docs/api/plugins/standalone_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.standalone_test` ------------------------------------------- - -.. automodule:: certbot.plugins.standalone_test - :members: diff --git a/docs/api/plugins/util_test.rst b/docs/api/plugins/util_test.rst deleted file mode 100644 index cce425a38..000000000 --- a/docs/api/plugins/util_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.util_test` ------------------------------------- - -.. automodule:: certbot.plugins.util_test - :members: diff --git a/docs/api/plugins/webroot_test.rst b/docs/api/plugins/webroot_test.rst deleted file mode 100644 index 893ea3067..000000000 --- a/docs/api/plugins/webroot_test.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.webroot_test` ---------------------------------------- - -.. automodule:: certbot.plugins.webroot_test - :members: From 5318945267ec3a8926210ec5fec2108bb267f269 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 5 Jul 2017 09:25:44 -0400 Subject: [PATCH 097/125] Hide exceptions that occur during session.close() (#4891) * Hide exceptions that occur during session.close() This fixes #4840. Exceptions that are raised out of __del__ methods are caught and printed to stderr. By catching any exceptions that occur, we now prevent this from happening. Alternative solutions to this would have been either not calling session.close() at all or adding a close() method to acme.client.ClientNetwork, acme.client.Client, and certbot.client.Client and using certbot.client.Client in a context manager to ensure close() is called. The former means that users of the ACME library never properly close their connections until their program exits and the latter adds a lot of complexity and nesting of client code for little benefit. * Only catch Exceptions --- acme/acme/client.py | 7 ++++++- acme/acme/client_test.py | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 9455159de..fa903f0e6 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -519,7 +519,12 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes self._default_timeout = timeout def __del__(self): - self.session.close() + # Try to close the session, but don't show exceptions to the + # user if the call to close() fails. See #4840. + try: + self.session.close() + except Exception: # pylint: disable=broad-except + pass def _wrap_in_jws(self, obj, nonce): """Wrap `JSONDeSerializable` object in JWS. diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index cd1a90645..54652b46c 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -600,12 +600,19 @@ class ClientNetworkTest(unittest.TestCase): mock.ANY, mock.ANY, verify=mock.ANY, headers=mock.ANY, timeout=45) - def test_del(self): + def test_del(self, close_exception=None): sess = mock.MagicMock() + + if close_exception is not None: + sess.close.side_effect = close_exception + self.net.session = sess del self.net sess.close.assert_called_once_with() + def test_del_error(self): + self.test_del(ReferenceError) + @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): mock_requests.exceptions = requests.exceptions From bf763cbbc6c1255086b16343d0b2a08757f54911 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 5 Jul 2017 10:00:14 -0400 Subject: [PATCH 098/125] remove outdated error message --- certbot-apache/certbot_apache/configurator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 097c4ff42..9e3eb4139 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -347,9 +347,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): vhost = display_ops.select_vhost(target_name, self.vhosts) if vhost is None: logger.error( - "No vhost exists with servername or alias of: %s " - "(or it's in a file with multiple vhosts, which Certbot " - "can't parse yet). " + "No vhost exists with servername or alias of %s. " "No vhost was selected. Please specify ServerName or ServerAlias " "in the Apache config, or split vhosts into separate files.", target_name) From 72b1a6f9cd3be08adb447b0029afcd7b81e27caa Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 5 Jul 2017 10:03:02 -0400 Subject: [PATCH 099/125] Update outdated comment --- certbot-apache/certbot_apache/tls_sni_01.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index 65a66d2fd..da64400b1 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -126,9 +126,8 @@ class ApacheTlsSni01(common.TLSSNI01): vhost = self.configurator.choose_vhost(achall.domain, temp=True) except (PluginError, MissingCommandlineFlag): # We couldn't find the virtualhost for this domain, possibly - # because it's a new vhost that's not configured yet (GH #677), - # or perhaps because there were multiple sections - # in the config file (GH #1042). See also GH #2600. + # because it's a new vhost that's not configured yet + # (GH #677). See also GH #2600. logger.warning("Falling back to default vhost %s...", default_addr) addrs.add(default_addr) return addrs From 6bb95c65960a517244e37e76634858fc415276a7 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Wed, 5 Jul 2017 11:59:23 -0500 Subject: [PATCH 100/125] Fix a typo: enviroment -> environment (#4898) --- docs/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index a1e91c010..e75a8b3e2 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -89,7 +89,7 @@ You can workaround this restriction by creating a temporary swapfile:: user@webserver:~$ sudo mkswap /tmp/swapfile user@webserver:~$ sudo swapon /tmp/swapfile -Disable and remove the swapfile once the virtual enviroment is constructed:: +Disable and remove the swapfile once the virtual environment is constructed:: user@webserver:~$ sudo swapoff /tmp/swapfile user@webserver:~$ sudo rm /tmp/swapfile From f314ea1d33b7d3e9e054b5e36da2a0f17984d9a1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 6 Jul 2017 10:30:29 -0400 Subject: [PATCH 101/125] s/renew-hook/deploy-hook docs/using.rst --- docs/using.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 0b2206db2..abdf5c619 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -397,15 +397,15 @@ unnecessarily stopping your webserver. ``--pre-hook`` and ``--post-hook`` hooks run before and after every renewal attempt. If you want your hook to run only after a successful renewal, use -``--renew-hook`` in a command like this. +``--deploy-hook`` in a command like this. -``certbot renew --renew-hook /path/to/renew-hook-script`` +``certbot renew --deploy-hook /path/to/deploy-hook-script`` For example, if you have a daemon that does not read its certificates as the root user, a renew hook like this can copy them to the correct location and apply appropriate file permissions. -/path/to/renew-hook-script +/path/to/deploy-hook-script .. code-block:: none @@ -504,7 +504,7 @@ renewal configuration file, located at ``/etc/letsencrypt/renewal/CERTNAME``. .. warning:: Modifying any files in ``/etc/letsencrypt`` can damage them so Certbot can no longer properly manage its certificates, and we do not recommend doing so. For most tasks, it is safest to limit yourself to pointing symlinks at the files there, or using -``--renew-hook`` to copy / make new files based upon those files, if your operational situation requires it +``--deploy-hook`` to copy / make new files based upon those files, if your operational situation requires it (for instance, combining certificates and keys in different way, or having copies of things with different specific permissions that are demanded by other programs). @@ -598,7 +598,7 @@ The following files are available: .. 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_. + ``--deploy-hook`` if you're using automatic renewal_. .. _hooks: From c3c1609fa025b3628c9908fd8ee3f1fa67adcdba Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 6 Jul 2017 14:59:28 -0400 Subject: [PATCH 102/125] no more renew(al) hook(s) --- docs/using.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index abdf5c619..ef87157aa 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -402,7 +402,7 @@ attempt. If you want your hook to run only after a successful renewal, use ``certbot renew --deploy-hook /path/to/deploy-hook-script`` For example, if you have a daemon that does not read its certificates as the -root user, a renew hook like this can copy them to the correct location and +root user, a deploy hook like this can copy them to the correct location and apply appropriate file permissions. /path/to/deploy-hook-script @@ -438,7 +438,7 @@ apply appropriate file permissions. esac done -More information about renewal hooks can be found by running +More information about hooks can be found by running ``certbot --help renew``. If you're sure that this command executes successfully without human From b23384438f1bcb7302c6679306cd3ad64b919da8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 6 Jul 2017 15:46:21 -0400 Subject: [PATCH 103/125] update changelog for 0.16.0 release (#4906) --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f196ec7..0a33b053a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,48 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). +## 0.16.0 - 2017-07-05 + +### Added + +* A plugin for performing DNS challenges using dynamic DNS updates as defined + in RFC 2316. This plugin is packaged separately from Certbot and is available + at https://pypi.python.org/pypi/certbot-dns-rfc2136. It supports Python 2.6, + 2.7, and 3.3+. At this time, there isn't a good way to install this plugin + when using certbot-auto, but this should change in the near future. +* Plugins for performing DNS challenges for the providers + [DNS Made Easy](https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy) and + [LuaDNS](https://pypi.python.org/pypi/certbot-dns-luadns). These plugins are + packaged separately from Certbot and support Python 2.7 and 3.3+. Currently, + there isn't a good way to install these plugins when using certbot-auto, + but that should change soon. +* Support for performing TLS-SNI-01 challenges when using the manual plugin. +* Automatic detection of Arch Linux in the Apache plugin providing better + default settings for the plugin. + +### Changed + +* The text of the interactive question about whether a redirect from HTTP to + HTTPS should be added by Certbot has been rewritten to better explain the + choices to the user. +* Simplified HTTP challenge instructions in the manual plugin. + +### Fixed + +* Problems performing a dry run when using the Nginx plugin have been fixed. +* Resolved an issue where certbot-dns-digitalocean's test suite would sometimes + fail when ran using Python 3. +* On some systems, previous versions of certbot-auto would error out with a + message about a missing hash for setuptools. This has been fixed. +* A bug where Certbot would sometimes not print a space at the end of an + interactive prompt has been resolved. +* Nonfatal tracebacks are no longer shown in rare cases where Certbot + encounters an exception trying to close its TCP connection with the ACME + server. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.16.0+is%3Aclosed + ## 0.15.0 - 2017-06-08 ### Added From 57e56cc97beaae3d6686c78cdcd82f4ee70d5569 Mon Sep 17 00:00:00 2001 From: ohemorange Date: Thu, 6 Jul 2017 15:57:11 -0700 Subject: [PATCH 104/125] Candidate 0.16.0 (#4908) * Release 0.16.0 * Bump version to 0.17.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 126 ++++++++++-------- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-dnsmadeeasy/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-luadns/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-rfc2136/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 60 +++++++-- letsencrypt-auto | 126 ++++++++++-------- letsencrypt-auto-source/certbot-auto.asc | 14 +- letsencrypt-auto-source/letsencrypt-auto | 26 ++-- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/certbot-requirements.txt | 24 ++-- 22 files changed, 237 insertions(+), 169 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 4e6eaf50c..0b166dd91 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.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 64fd8be66..7f61f9041 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.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 725c86895..599538891 100755 --- a/certbot-auto +++ b/certbot-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.15.0" +LE_AUTO_VERSION="0.16.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -710,54 +710,66 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -cffi==1.4.2 \ - --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ - --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ - --hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \ - --hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \ - --hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \ - --hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \ - --hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \ - --hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \ - --hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \ - --hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \ - --hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \ - --hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \ - --hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \ - --hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \ - --hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \ - --hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998 +cffi==1.10.0 \ + --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ + --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ + --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ + --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ + --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ + --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ + --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ + --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ + --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ + --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ + --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ + --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ + --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ + --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ + --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ + --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ + --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ + --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ + --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ + --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ + --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ + --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ + --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ + --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ + --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ + --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ + --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ + --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ + --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ + --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ + --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ + --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ + --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ + --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ + --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ + --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.8.2 \ - --hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \ - --hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \ - --hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \ - --hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \ - --hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \ - --hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \ - --hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \ - --hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \ - --hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \ - --hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \ - --hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \ - --hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \ - --hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \ - --hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \ - --hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \ - --hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \ - --hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \ - --hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \ - --hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \ - --hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \ - --hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \ - --hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \ - --hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \ - --hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \ - --hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \ - --hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda +cryptography==1.9 \ + --hash=sha256:469a9d3d851038f1eb7d7f77bb08bb4775b41483372be450e25b293fe57bd59e \ + --hash=sha256:533143321d15c8743f91eec5c5f495c1b5cad9a25de8f6dab01eddd6b416903e \ + --hash=sha256:2230c186182d773064d06242e0fa604cd718bfff28aa9c5ae73d7e426e98a151 \ + --hash=sha256:3dc94ed5a26b8553a325767358f505c0a43e0c89df078647f77a4d022ddcdc57 \ + --hash=sha256:2eb8297b877cb6b56216750fa7017c9f5786bec8afd6a0f1aaace02cbfb6195f \ + --hash=sha256:025a96e48164106f2082b00f42bf430cf21f09e203e42585a712e420b75cbff0 \ + --hash=sha256:61eb3534f8ed2415dd708b28919205d523f220e4cd5b8165844edfdd4a649b8e \ + --hash=sha256:5474fe5ce6b517d3086e0231b6ad88f8978c551c4379f91c3d619c308490f0d7 \ + --hash=sha256:5ff169869624e23767d70db274f13a9ea4e97c029425a1224aa5e049e84ce2af \ + --hash=sha256:c26e057a2de13e97e708328d295c5ac4cd3eab4a5c42ce727dd1a53316034b8a \ + --hash=sha256:7db719432648f14ea33edffc5f75330c595804eac86ca916528b35ce50a8bfd6 \ + --hash=sha256:ca72537a7064bb80e34b6908e576ffc8e2c2cad29a7168f48d0999df089695c3 \ + --hash=sha256:2a5e577f5d2093e51486b4ec02b51bb5adb165b98fee99929d5af0813e90b469 \ + --hash=sha256:9d9da8bac2e31003d092f5ef6981a725c77c4e9a30638436884d61ad39f9a1ee \ + --hash=sha256:fab8ec6866e384d9827d5dc02a1383e991a6c05c54f818316c4b829e56ca2ba3 \ + --hash=sha256:365eb804362e581c9a02e7a610b30514f07dd77b2a38aed338eb5192446bbc58 \ + --hash=sha256:2908f709f02711dbb10561a9f154d2f7d1792385e2341e9708539cc4ecfb8667 \ + --hash=sha256:5518337022718029e367d982642f3e3523541e098ad671672a90b82474c84882 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 @@ -864,18 +876,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.15.0 \ - --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ - --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb -acme==0.15.0 \ - --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ - --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 -certbot-apache==0.15.0 \ - --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ - --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca -certbot-nginx==0.15.0 \ - --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ - --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 +certbot==0.16.0 \ + --hash=sha256:e1cc2479f4f149a7128b67192c3f5f6c111b6b9ddcac421ebee8ac5030d5b09b \ + --hash=sha256:795801fd6b06b32060e364ac045312e6b26c6272d5ca32878277e5a2afdee186 +acme==0.16.0 \ + --hash=sha256:31592a744f7a6e7cbb4c5daf2deebc9af6b03997d6f80e5becc203ab8694edcf \ + --hash=sha256:538b69134cc50bdcdcc081b844c85158509022c61d7abc1f44216beeb95de9cd +certbot-apache==0.16.0 \ + --hash=sha256:337f84f391c7d7c31d84376e7a8c882ac119112a4aabceadd1a7de6a2f01ca06 \ + --hash=sha256:f1f37b56e1ceb0a28ccf51d54ca34444e6a708448845ef25372e223b9a82c4d4 +certbot-nginx==0.16.0 \ + --hash=sha256:1d57428202f8e7abdd99c3ddbcf374aad5f69c4262fe8dae53d2016e4ae04ded \ + --hash=sha256:77711655514d707ddf728195f00b2f6cdd1ad9db52440276b4a67664479c6954 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 2b2796704..071b97d22 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.16.0.dev0' +version = '0.17.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 58444b5c1..7162a1dcf 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 41282f35c..024e32918 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 6b689f660..dc09fdd0b 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 14f27c960..98d3e0a25 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index f3b9b8151..a3ab24684 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index ee20fd1fd..4b7d21f2e 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index c964b01da..be0774e13 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 5c7bb2b5a..addb69b6d 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index e1d54e4bd..3b5409602 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 5f6f61e4e..7a69f9a13 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -3,7 +3,7 @@ import sys from distutils.core import setup from setuptools import find_packages -version = '0.16.0.dev0' +version = '0.17.0.dev0' install_requires = [ 'acme=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 699c6bba6..b501aecd9 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.16.0.dev0' +version = '0.17.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index b372841e0..23ed7eb7a 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.16.0.dev0' +__version__ = '0.17.0.dev0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 4265056ce..6ff284788 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -89,7 +89,7 @@ optional arguments: case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/0.15.0 (certbot; + "". (default: CertbotACMEClient/0.16.0 (certbot; Ubuntu 16.04.2 LTS) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/2.7.12). The flags encoded in the user agent are: --duplicate, --force- @@ -397,15 +397,21 @@ plugins: using DigitalOcean for DNS). (default: False) --dns-dnsimple Obtain certificates using a DNS TXT record (if you are using DNSimple for DNS). (default: False) + --dns-dnsmadeeasy Obtain certificates using a DNS TXT record (if you + areusing DNS Made Easy for DNS). (default: False) --dns-google Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS). (default: False) + --dns-luadns Obtain certificates using a DNS TXT record (if you are + using LuaDNS for DNS). (default: False) --dns-nsone Obtain certificates using a DNS TXT record (if you are using NS1 for DNS). (default: False) + --dns-rfc2136 Obtain certificates using a DNS TXT record (if you are + using BIND for DNS). (default: False) --dns-route53 Obtain certificates using a DNS TXT record (if you are using Route53 for DNS). (default: False) apache: - Apache Web Server plugin + Apache Web Server plugin - Beta --apache-enmod APACHE_ENMOD Path to the Apache 'a2enmod' binary. (default: @@ -487,6 +493,17 @@ dns-dnsimple: --dns-dnsimple-credentials DNS_DNSIMPLE_CREDENTIALS DNSimple credentials INI file. (default: None) +dns-dnsmadeeasy: + Obtain certificates using a DNS TXT record (if you are using DNS Made Easy + for DNS). + + --dns-dnsmadeeasy-propagation-seconds DNS_DNSMADEEASY_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 60) + --dns-dnsmadeeasy-credentials DNS_DNSMADEEASY_CREDENTIALS + DNS Made Easy credentials INI file. (default: None) + dns-google: Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS for DNS). @@ -504,6 +521,17 @@ dns-google: control#permissions_and_roles for information about therequired permissions.) (default: None) +dns-luadns: + Obtain certificates using a DNS TXT record (if you are using LuaDNS for + DNS). + + --dns-luadns-propagation-seconds DNS_LUADNS_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 30) + --dns-luadns-credentials DNS_LUADNS_CREDENTIALS + LuaDNS credentials INI file. (default: None) + dns-nsone: Obtain certificates using a DNS TXT record (if you are using NS1 for DNS). @@ -514,6 +542,17 @@ dns-nsone: --dns-nsone-credentials DNS_NSONE_CREDENTIALS NS1 credentials file. (default: None) +dns-rfc2136: + Obtain certificates using a DNS TXT record (if you are using BIND for + DNS). + + --dns-rfc2136-propagation-seconds DNS_RFC2136_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 60) + --dns-rfc2136-credentials DNS_RFC2136_CREDENTIALS + RFC 2136 credentials INI file. (default: None) + dns-route53: Obtain certificates using a DNS TXT record (if you are using AWS Route53 for DNS). @@ -526,11 +565,16 @@ dns-route53: manual: 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 + environment variables available to this script depend on the type of + challenge. $CERTBOT_DOMAIN will always contain the domain being + authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION is the + validation string, and $CERTBOT_TOKEN is the filename of the resource + requested when performing an HTTP-01 challenge. When performing a TLS- + SNI-01 challenge, $CERTBOT_SNI_DOMAIN will contain the SNI name for which + the ACME server expects to be presented with the self-signed certificate + located at $CERTBOT_CERT_PATH. The secret key needed to complete the TLS + handshake is located at $CERTBOT_KEY_PATH. 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. @@ -544,7 +588,7 @@ manual: Automatically allows public IP logging (default: Ask) nginx: - Nginx Web Server plugin + Nginx Web Server plugin - Alpha --nginx-server-root NGINX_SERVER_ROOT Nginx server root directory. (default: /etc/nginx) diff --git a/letsencrypt-auto b/letsencrypt-auto index 725c86895..599538891 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.15.0" +LE_AUTO_VERSION="0.16.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -710,54 +710,66 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -cffi==1.4.2 \ - --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ - --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ - --hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \ - --hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \ - --hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \ - --hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \ - --hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \ - --hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \ - --hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \ - --hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \ - --hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \ - --hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \ - --hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \ - --hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \ - --hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \ - --hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998 +cffi==1.10.0 \ + --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ + --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ + --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ + --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ + --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ + --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ + --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ + --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ + --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ + --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ + --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ + --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ + --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ + --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ + --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ + --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ + --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ + --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ + --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ + --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ + --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ + --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ + --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ + --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ + --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ + --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ + --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ + --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ + --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ + --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ + --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ + --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ + --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ + --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ + --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ + --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.8.2 \ - --hash=sha256:e572527dc4eae300d4ac58c3a49fd0fe1a0178baf341f536d22c45455c958410 \ - --hash=sha256:15448bcfc4ef0f58c8e049f06cb10c296d75456ced02466dff3c82cc9c85f0a6 \ - --hash=sha256:771171a4b7677ee791f74928030fb59ca83a1710d32eaec8395c5170fc520741 \ - --hash=sha256:06e47faef940bc53ca23f5c6a29f5d4ebc47f0c7632f356da8ce4cc3ae99e908 \ - --hash=sha256:730a4f2c028b33b3e6b1a3caa7a3048a1e1e6ff2fe9043acdb21b31a2e711742 \ - --hash=sha256:1bf2299033c5a517014ffd5ec2e267f6a220d9095e75dd002dc33a898a7857cc \ - --hash=sha256:5e7249bc0360d834b89631e83c1a7bbb28098b59fab9816e5e19efdef7b71a1c \ - --hash=sha256:3971bdb5054257c922d95839d10ad347dcaa7137efeed34ce33ee660ea52b7e2 \ - --hash=sha256:a8b431f82972cec974766a484eba02d7bbf6a5c042c13c25f1a23d4a3a31bfb4 \ - --hash=sha256:a9281f0292131747f94219793438d78823bb72fbcafd1b415e99af1d8c42e11c \ - --hash=sha256:502237c0ed9ca77212cf1673627fd2c361ee989cdde2ac54a0cd3f17cbc79e5a \ - --hash=sha256:c63625ec36d1615fff69b068a95ea038d5f8df961901a097dfedc7e7410794d5 \ - --hash=sha256:bda0a32d99ee1f86fcd46bbb10f43216f101df3349187ea8999967cddbfede86 \ - --hash=sha256:a4a69088671eb31aa292a5996d9dd7a4ccb585b6fc7eb7b9e47051e1169fc479 \ - --hash=sha256:0f7127b0034d5112b190de6bf46fadc41940983a91836acfdaa16c44f44beb75 \ - --hash=sha256:9049c073f86155dfcd93434d63db80f753cd2e5bebf1d6172b112de215369b07 \ - --hash=sha256:264dd80150f24a6fffb3ce5be32705e6a27160df055b3582925c2ed170f4f430 \ - --hash=sha256:a05f899c311f6810ae4981edcd23d1587be207de554cf0530f8facbe836483cb \ - --hash=sha256:91970de4b3dbf0b9b36745e9f346265d225d906188dec3d02c2179fbdb49b167 \ - --hash=sha256:908cd59ae3c177c28e7a3eb519dade45748ba9af9959796c699f4f1b56caea8d \ - --hash=sha256:f04fd7c4b7b4c0b97186f31a315fe88d20087a7148ff06a9c0348b35e39531f8 \ - --hash=sha256:757dd412a49ea396b52b051c364bf8f9262dfa6665270f68208a0d6ed5999f1b \ - --hash=sha256:7d6ab4507cf52328b27c57107491b2699a5e25097213a2d201fab0157cb5dd09 \ - --hash=sha256:e3183550d129b7103914cad049a3b2358d9405c6e4baf3a7c92a82004f5e4814 \ - --hash=sha256:9d1f63e2f4530a919ef87b4f1b3d814389604486f8b8c090aefccace4d1f98f8 \ - --hash=sha256:8e88ebac371a388024dab3ccf393bf3c1790d21bc3c299d5a6f9f83fb823beda +cryptography==1.9 \ + --hash=sha256:469a9d3d851038f1eb7d7f77bb08bb4775b41483372be450e25b293fe57bd59e \ + --hash=sha256:533143321d15c8743f91eec5c5f495c1b5cad9a25de8f6dab01eddd6b416903e \ + --hash=sha256:2230c186182d773064d06242e0fa604cd718bfff28aa9c5ae73d7e426e98a151 \ + --hash=sha256:3dc94ed5a26b8553a325767358f505c0a43e0c89df078647f77a4d022ddcdc57 \ + --hash=sha256:2eb8297b877cb6b56216750fa7017c9f5786bec8afd6a0f1aaace02cbfb6195f \ + --hash=sha256:025a96e48164106f2082b00f42bf430cf21f09e203e42585a712e420b75cbff0 \ + --hash=sha256:61eb3534f8ed2415dd708b28919205d523f220e4cd5b8165844edfdd4a649b8e \ + --hash=sha256:5474fe5ce6b517d3086e0231b6ad88f8978c551c4379f91c3d619c308490f0d7 \ + --hash=sha256:5ff169869624e23767d70db274f13a9ea4e97c029425a1224aa5e049e84ce2af \ + --hash=sha256:c26e057a2de13e97e708328d295c5ac4cd3eab4a5c42ce727dd1a53316034b8a \ + --hash=sha256:7db719432648f14ea33edffc5f75330c595804eac86ca916528b35ce50a8bfd6 \ + --hash=sha256:ca72537a7064bb80e34b6908e576ffc8e2c2cad29a7168f48d0999df089695c3 \ + --hash=sha256:2a5e577f5d2093e51486b4ec02b51bb5adb165b98fee99929d5af0813e90b469 \ + --hash=sha256:9d9da8bac2e31003d092f5ef6981a725c77c4e9a30638436884d61ad39f9a1ee \ + --hash=sha256:fab8ec6866e384d9827d5dc02a1383e991a6c05c54f818316c4b829e56ca2ba3 \ + --hash=sha256:365eb804362e581c9a02e7a610b30514f07dd77b2a38aed338eb5192446bbc58 \ + --hash=sha256:2908f709f02711dbb10561a9f154d2f7d1792385e2341e9708539cc4ecfb8667 \ + --hash=sha256:5518337022718029e367d982642f3e3523541e098ad671672a90b82474c84882 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 @@ -864,18 +876,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.15.0 \ - --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ - --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb -acme==0.15.0 \ - --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ - --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 -certbot-apache==0.15.0 \ - --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ - --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca -certbot-nginx==0.15.0 \ - --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ - --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 +certbot==0.16.0 \ + --hash=sha256:e1cc2479f4f149a7128b67192c3f5f6c111b6b9ddcac421ebee8ac5030d5b09b \ + --hash=sha256:795801fd6b06b32060e364ac045312e6b26c6272d5ca32878277e5a2afdee186 +acme==0.16.0 \ + --hash=sha256:31592a744f7a6e7cbb4c5daf2deebc9af6b03997d6f80e5becc203ab8694edcf \ + --hash=sha256:538b69134cc50bdcdcc081b844c85158509022c61d7abc1f44216beeb95de9cd +certbot-apache==0.16.0 \ + --hash=sha256:337f84f391c7d7c31d84376e7a8c882ac119112a4aabceadd1a7de6a2f01ca06 \ + --hash=sha256:f1f37b56e1ceb0a28ccf51d54ca34444e6a708448845ef25372e223b9a82c4d4 +certbot-nginx==0.16.0 \ + --hash=sha256:1d57428202f8e7abdd99c3ddbcf374aad5f69c4262fe8dae53d2016e4ae04ded \ + --hash=sha256:77711655514d707ddf728195f00b2f6cdd1ad9db52440276b4a67664479c6954 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 9c2014502..aa713c8c0 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v2 -iQEcBAABCAAGBQJZOXvxAAoJEE0XyZXNl3XyAEEH/3S7/j7XNYv3lfy959ef/vFm -xntReU0J9sOpCLPFHmX2/AFSzxc+L70d082+di50uqnTJHEadvVsnb+XURLAhMeD -GmVjrFkAiigvhX3xWIP3Iu9m/r+wp4h2G6Z6duXcNLhra3Z06n2IxD5rIYG118gH -OnVweYhg6e14B4nbVJBQ2XZp2NfIf9RFE973CQk3YC/cMG4Vfj17n6z8e5nuMZhb -Ztxo9YttWhKfGp7BgbWLZaPJ4XE7LETBYVbBT5V5bSl7ktmftXcOwIUgYcevvZqe -1OLLI8KBLW7OUAuKyivfksn/sIaJY1vvKfj+UruqMCuVGwO8lr4BXKBZxMaq5fk= -=mmbE +iQEcBAABCAAGBQJZXV5PAAoJEE0XyZXNl3Xy3IgH/1OiY21IcAGx13jW32KNIsf9 +/vqjR/fyPbraSzYDldCVIQxsaVA6oh2kNZgiB11OpPT20/WsDq1+Ymj+dMjonJfm +w3wjx5AnfR/YThQwNXUJ83XnPCA78CtYiXus9gyqk+10WpNXUkbdGOwM4eYOtb3o +fNjJXkA00pATIYXks6qV0WJVEDNYuHDfkDfxukVgU7HzjfayLQjo4Zbs7Qnp7oH9 ++Lizc11nTliUUo3hHyLziJsCfC+Irso5Q/fHM9rn9DS360PcW0uNjWeLQk0U1w1l +tuuObOsDi/7Rejk4uDu6qDVemdEoG1btCrZgzOvzi3NstWigrL76cZuoz0gaGu4= +=mN1c -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 3e5db44ff..276d68c22 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -28,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.16.0.dev0" +LE_AUTO_VERSION="0.17.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -876,18 +876,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.15.0 \ - --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ - --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb -acme==0.15.0 \ - --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ - --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 -certbot-apache==0.15.0 \ - --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ - --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca -certbot-nginx==0.15.0 \ - --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ - --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 +certbot==0.16.0 \ + --hash=sha256:e1cc2479f4f149a7128b67192c3f5f6c111b6b9ddcac421ebee8ac5030d5b09b \ + --hash=sha256:795801fd6b06b32060e364ac045312e6b26c6272d5ca32878277e5a2afdee186 +acme==0.16.0 \ + --hash=sha256:31592a744f7a6e7cbb4c5daf2deebc9af6b03997d6f80e5becc203ab8694edcf \ + --hash=sha256:538b69134cc50bdcdcc081b844c85158509022c61d7abc1f44216beeb95de9cd +certbot-apache==0.16.0 \ + --hash=sha256:337f84f391c7d7c31d84376e7a8c882ac119112a4aabceadd1a7de6a2f01ca06 \ + --hash=sha256:f1f37b56e1ceb0a28ccf51d54ca34444e6a708448845ef25372e223b9a82c4d4 +certbot-nginx==0.16.0 \ + --hash=sha256:1d57428202f8e7abdd99c3ddbcf374aad5f69c4262fe8dae53d2016e4ae04ded \ + --hash=sha256:77711655514d707ddf728195f00b2f6cdd1ad9db52440276b4a67664479c6954 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 96ad67971c7681b15b10f0eaf3eb9cdb0b6fa999..72ade22dc968edccc5def0ef8890ce432ac28c37 100644 GIT binary patch literal 256 zcmV+b0ssEJHz@g#nD8PGws5j!$?m^r05$yytn(S`HvAjBbZ}iCxF^cG)KPocas;*P z^Kj%Qu2POy7QXi+B$$SugN=mN8QGmjifT18TDGi)q)E}1-heABV5&prX8K!~O`_o4 zj+OR9(M(OkbX0Qqi_IQUSh~gSFT9cIVBKcZV1!qa72N7PjZPqzAt80cZ~=?45o9pq~DzcX%?T+f76vxd1SH@e?cbnz9V7Gk{#$I_qlph1%RWSG-^`UlbnZuGSID1Xo%_C5H;Lh=+qz GN5G#rse^w2 literal 256 zcmV+b0ssD3fX@-nqy*RZKiKGM*9`hKiI~uOv?z@%8e6eoXH#N9%&*n0r(iyCPr(6X z?ZGwB-MR{ATLE6;NnZHM`?|?m8FMwuI{!yQ+!<+ugPHW8uT5dB%z|6H+_M&FlXaTg z_hMunHnrRqT;VQ=xi}bk^BulkXyn(;*#?ps{JJWnCeiiM+3EUC0q`^N@6G;bqF3W% zN0;NrCOOa$BYs{-Q0NkUlWKAtlpd2Ly?c|d6|XfWu}q(Z1&sw$=RJPMqX`pNb&TE` zCU>Eit~$<>R6o*ABsmKrEF#CsN$+V4uO)i^IKukbQgERL>jngxCDs7D(ZI%(;07_T GP?_#C6M&xp diff --git a/letsencrypt-auto-source/pieces/certbot-requirements.txt b/letsencrypt-auto-source/pieces/certbot-requirements.txt index da9c33583..3c91c6e76 100644 --- a/letsencrypt-auto-source/pieces/certbot-requirements.txt +++ b/letsencrypt-auto-source/pieces/certbot-requirements.txt @@ -1,12 +1,12 @@ -certbot==0.15.0 \ - --hash=sha256:f052a1ee9d0e71b73d893c26ee3aa343f6f3abe7de8471437779d541f8bf7824 \ - --hash=sha256:b8c4043b2b8df39660d4ce4a2a6eca590f98ece0e1b97eba53ab95f3bbac3beb -acme==0.15.0 \ - --hash=sha256:d423a14a8fde089d6854ccbe1314f6a80553ef06799ac6f671b90d8399835e60 \ - --hash=sha256:9fadd63322a1eb95f58e6cda8ca2095c750e828ae470bc6e3925ef618c7cfc87 -certbot-apache==0.15.0 \ - --hash=sha256:07fa99b264e0ea489695cc2a5353f3fe9459422ad549de02c46da24ae546acd9 \ - --hash=sha256:6da1433381bd2c2ea7c395be57ca1bcdb7c1c04ce3d12b67a3fa207a3bfa41ca -certbot-nginx==0.15.0 \ - --hash=sha256:7b0622f8a9031e24105f9b5c8d98d4ed83ae393517ed35cc2a4fa50098122922 \ - --hash=sha256:0b98dedb22492d6f88dffdfbd721b4d4c98bfe361df35bc97bf5bd3047f01234 +certbot==0.16.0 \ + --hash=sha256:e1cc2479f4f149a7128b67192c3f5f6c111b6b9ddcac421ebee8ac5030d5b09b \ + --hash=sha256:795801fd6b06b32060e364ac045312e6b26c6272d5ca32878277e5a2afdee186 +acme==0.16.0 \ + --hash=sha256:31592a744f7a6e7cbb4c5daf2deebc9af6b03997d6f80e5becc203ab8694edcf \ + --hash=sha256:538b69134cc50bdcdcc081b844c85158509022c61d7abc1f44216beeb95de9cd +certbot-apache==0.16.0 \ + --hash=sha256:337f84f391c7d7c31d84376e7a8c882ac119112a4aabceadd1a7de6a2f01ca06 \ + --hash=sha256:f1f37b56e1ceb0a28ccf51d54ca34444e6a708448845ef25372e223b9a82c4d4 +certbot-nginx==0.16.0 \ + --hash=sha256:1d57428202f8e7abdd99c3ddbcf374aad5f69c4262fe8dae53d2016e4ae04ded \ + --hash=sha256:77711655514d707ddf728195f00b2f6cdd1ad9db52440276b4a67664479c6954 From d0ecf739bd5f3f003ee3b457aab7477cc86fb814 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Fri, 7 Jul 2017 06:46:09 -0500 Subject: [PATCH 105/125] Add new DNS authenticator plugins in 0.16 (#4911) --- docs/packaging.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/packaging.rst b/docs/packaging.rst index cc43ee71a..3d58ea92e 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -15,8 +15,11 @@ We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.python.org/pypi/certbot-dns-cloudxns - https://pypi.python.org/pypi/certbot-dns-digitalocean - https://pypi.python.org/pypi/certbot-dns-dnsimple +- https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy - https://pypi.python.org/pypi/certbot-dns-google +- https://pypi.python.org/pypi/certbot-dns-luadns - https://pypi.python.org/pypi/certbot-dns-nsone +- https://pypi.python.org/pypi/certbot-dns-rfc2136 - https://pypi.python.org/pypi/certbot-dns-route53 The following scripts are used in the process: @@ -58,8 +61,11 @@ From our official releases: - https://www.archlinux.org/packages/community/any/certbot-dns-cloudxns - https://www.archlinux.org/packages/community/any/certbot-dns-digitalocean - https://www.archlinux.org/packages/community/any/certbot-dns-dnsimple +- https://www.archlinux.org/packages/community/any/certbot-dns-dnsmadeeasy - https://www.archlinux.org/packages/community/any/certbot-dns-google +- https://www.archlinux.org/packages/community/any/certbot-dns-luadns - https://www.archlinux.org/packages/community/any/certbot-dns-nsone +- https://www.archlinux.org/packages/community/any/certbot-dns-rfc2136 - https://www.archlinux.org/packages/community/any/certbot-dns-route53 From ``master``: https://aur.archlinux.org/packages/certbot-git From 48ef16ab0d6e05473c6daf27ec793f153bc9afc6 Mon Sep 17 00:00:00 2001 From: Florian Mutter Date: Fri, 7 Jul 2017 21:46:30 +0200 Subject: [PATCH 106/125] Align domain names output to command line input (#4874) The command line takes a comma separated list of domain names. To be able to use the list of existing domain names it would be helpful to get a list that is also comma separated. Sample use case: If you would like to add a new domain to an existing certificate you need to list all existing domain names. Makes certbot certificates use comma-separated domain names instead of space-separated. --- certbot/cert_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index 62cbfa695..207e4d072 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -205,7 +205,7 @@ def _report_human_readable(config, parsed_certs): " Certificate Path: {3}\n" " Private Key Path: {4}".format( cert.lineagename, - " ".join(cert.names()), + ",".join(cert.names()), valid_string, cert.fullchain, cert.privkey)) From 62bdf663f2ec45379c6d15fc92b83536517f33e0 Mon Sep 17 00:00:00 2001 From: Baime Date: Sat, 8 Jul 2017 16:20:12 +0200 Subject: [PATCH 107/125] Check keys if revoke certificate by private key --- certbot/crypto_util.py | 15 ++++++++------- certbot/main.py | 1 + certbot/tests/crypto_util_test.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index e22effeb7..2c9dad8bf 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -214,7 +214,7 @@ def verify_renewable_cert(renewable_cert): """ verify_renewable_cert_sig(renewable_cert) verify_fullchain(renewable_cert) - verify_cert_matches_priv_key(renewable_cert) + verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey) def verify_renewable_cert_sig(renewable_cert): @@ -238,17 +238,18 @@ def verify_renewable_cert_sig(renewable_cert): raise errors.Error(error_str) -def verify_cert_matches_priv_key(renewable_cert): +def verify_cert_matches_priv_key(cert_path, key_path): """ Verifies that the private key and cert match. - :param `.storage.RenewableCert` renewable_cert: cert to verify + :param str cert_path: path to a cert in PEM format + :param str key_path: path to a private key file :raises errors.Error: If they don't match. """ try: - with open(renewable_cert.cert) as cert: + with open(cert_path) as cert: cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.read()) - with open(renewable_cert.privkey) as privkey: + with open(key_path) as privkey: privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey.read()) context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) context.use_privatekey(privkey) @@ -257,8 +258,8 @@ def verify_cert_matches_priv_key(renewable_cert): except (IOError, OpenSSL.SSL.Error) as e: error_str = "verifying the cert located at {0} matches the \ private key located at {1} has failed. \ - Details: {2}".format(renewable_cert.cert, - renewable_cert.privkey, e) + Details: {2}".format(cert_path, + key_path, e) logger.exception(error_str) raise errors.Error(error_str) diff --git a/certbot/main.py b/certbot/main.py index f7421d75e..c055b9ba9 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -562,6 +562,7 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config if config.key_path is not None: # revocation by cert key logger.debug("Revoking %s using cert key %s", config.cert_path[0], config.key_path[0]) + crypto_util.verify_cert_matches_priv_key(config.cert_path[0], config.key_path[1]) key = jose.JWK.load(config.key_path[1]) else: # revocation by account key logger.debug("Revoking %s using Account Key", config.cert_path[0]) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 729b09dc1..cb9077208 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -252,7 +252,7 @@ class VerifyCertMatchesPrivKeyTest(VerifyCertSetup): def _call(self, renewable_cert): from certbot.crypto_util import verify_cert_matches_priv_key - return verify_cert_matches_priv_key(renewable_cert) + return verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey) def test_cert_priv_key_match(self): self.assertEqual(None, self._call(self.renewable_cert)) From ab286e088728b349b5347c2f39fc8cba57d014d6 Mon Sep 17 00:00:00 2001 From: Baime Date: Sat, 8 Jul 2017 18:00:33 +0200 Subject: [PATCH 108/125] fixed path errors in revoking by key process --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index c055b9ba9..e7ed9bfe3 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -562,7 +562,7 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config if config.key_path is not None: # revocation by cert key logger.debug("Revoking %s using cert key %s", config.cert_path[0], config.key_path[0]) - crypto_util.verify_cert_matches_priv_key(config.cert_path[0], config.key_path[1]) + crypto_util.verify_cert_matches_priv_key(config.cert_path[0], config.key_path[0]) key = jose.JWK.load(config.key_path[1]) else: # revocation by account key logger.debug("Revoking %s using Account Key", config.cert_path[0]) From 3a9150a7bacc3ee67b762c6f42c91ec9bf86711f Mon Sep 17 00:00:00 2001 From: Baime Date: Sat, 8 Jul 2017 19:35:07 +0200 Subject: [PATCH 109/125] Fix for revoke cert by key process --- certbot/crypto_util.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 2c9dad8bf..112ef7c85 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -247,13 +247,9 @@ def verify_cert_matches_priv_key(cert_path, key_path): :raises errors.Error: If they don't match. """ try: - with open(cert_path) as cert: - cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.read()) - with open(key_path) as privkey: - privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey.read()) context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) - context.use_privatekey(privkey) - context.use_certificate(cert) + context.use_certificate_file(cert_path) + context.use_privatekey_file(key_path) context.check_privatekey() except (IOError, OpenSSL.SSL.Error) as e: error_str = "verifying the cert located at {0} matches the \ From 368beee8bfd9413771d770df0e1b562e9c9396dd Mon Sep 17 00:00:00 2001 From: Baime Date: Sun, 9 Jul 2017 12:10:35 +0200 Subject: [PATCH 110/125] Use correct key/cert for revoke by key test --- certbot/tests/crypto_util_test.py | 2 ++ certbot/tests/main_test.py | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index cb9077208..bc41ca950 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -255,6 +255,8 @@ class VerifyCertMatchesPrivKeyTest(VerifyCertSetup): return verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey) def test_cert_priv_key_match(self): + self.renewable_cert.cert = SS_CERT_PATH + self.renewable_cert.privkey = RSA2048_KEY_PATH self.assertEqual(None, self._call(self.renewable_cert)) def test_cert_priv_key_mismatch(self): diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 7c2016178..25f8d1631 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -37,6 +37,8 @@ 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")) +RSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem') +SS_CERT_PATH = test_util.vector_path('self_signed_cert.pem') class TestHandleIdenticalCerts(unittest.TestCase): @@ -1012,12 +1014,12 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me @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, + self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH, '--server', server, 'revoke']) - with open(KEY, 'rb') as f: + with open(RSA2048_KEY_PATH, '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: + with open(SS_CERT_PATH, '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( From fad1a4b576f9d6f3efdc44ac8d51fe21238db036 Mon Sep 17 00:00:00 2001 From: John Harlan Date: Wed, 8 Feb 2017 18:29:29 -0500 Subject: [PATCH 111/125] Add flags to configure log rotation * Add & enable --disable-log-rotation * Add --max-log-count * Correct max-log-count: remove action=store_true add type=int * Add logging to cli argument groups. * Add logging group to HELP_TOPICS in __init__ * Adjust line length * test simplifying to one argument --- certbot/cli.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index fd9378079..c74a8102f 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -440,8 +440,8 @@ 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"] + HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + HELP_TOPICS += list(self.VERBS) + self.COMMANDS_TOPICS + ["manage"] plugin_names = list(plugins) self.help_topics = HELP_TOPICS + plugin_names + [None] @@ -849,6 +849,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis helpful.add( None, "-t", "--text", dest="text_mode", action="store_true", help=argparse.SUPPRESS) + helpful.add( + # Note, 1 is subtracted from max_log_count in certbot/main.py + #> to correct an off by one error. + [None, "paths"], + "--max-log-count", dest="max_log_count", + type=int, default=1000, + help="Specifies the maximum number of certbot log files " + "that will be kept. 0 disables log rotation. 1 causes " + "only the log from the most recent run to be kept. " + "2+ enables log rotation at start of certbot execution.") helpful.add( [None, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive", dest="noninteractive_mode", action="store_true", From bc3765d6d0635efedf7b3162b9fa68f525c04e13 Mon Sep 17 00:00:00 2001 From: yomna Date: Mon, 10 Jul 2017 19:05:52 -0700 Subject: [PATCH 112/125] No longer mask failed challenge errors with encoding errors (#4867) * no longer masker failed challenge errors with encoding errors * simplifying through type-checking * bytes --- acme/acme/messages.py | 13 +++++++++---- acme/acme/messages_test.py | 17 ++++++++++------- certbot/achallenges.py | 1 - certbot/tests/errors_test.py | 11 +++++++++++ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 5784e8e11..4b4fa5003 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -1,5 +1,6 @@ """ACME protocol messages.""" import collections +import six from acme import challenges from acme import errors @@ -36,9 +37,13 @@ ERROR_TYPE_DESCRIPTIONS.update(dict( # add errors with old prefix, deprecate me 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)) + if isinstance(err, Error) and (err.typ is not None): + return (ERROR_PREFIX in err.typ) or (OLD_ERROR_PREFIX in err.typ) + else: + return False +@six.python_2_unicode_compatible class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. @@ -92,10 +97,10 @@ class Error(jose.JSONObjectWithFields, errors.Error): return code def __str__(self): - return ' :: '.join( - part for part in + return b' :: '.join( + part.encode('ascii', 'backslashreplace') for part in (self.typ, self.description, self.detail, self.title) - if part is not None) + if part is not None).decode() class _Constant(jose.JSONDeSerializable, collections.Hashable): # type: ignore diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index ab05a89b7..b4ce19a08 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -26,6 +26,7 @@ class ErrorTest(unittest.TestCase): 'type': ERROR_PREFIX + 'malformed', } self.error_custom = Error(typ='custom', detail='bar') + self.empty_error = Error() self.jobj_custom = {'type': 'custom', 'detail': 'bar'} def test_default_typ(self): @@ -45,12 +46,6 @@ class ErrorTest(unittest.TestCase): 'The request message was malformed', self.error.description) self.assertTrue(self.error_custom.description is None) - def test_str(self): - self.assertEqual( - '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) @@ -60,8 +55,16 @@ class ErrorTest(unittest.TestCase): 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)) + self.assertFalse(is_acme_error(self.empty_error)) + self.assertFalse(is_acme_error("must pet all the {dogs|rabbits}")) + + def test_unicode_error(self): + from acme.messages import Error, ERROR_PREFIX, is_acme_error + arabic_error = Error( + detail=u'\u0639\u062f\u0627\u0644\u0629', typ=ERROR_PREFIX + 'malformed', + title='title') + self.assertTrue(is_acme_error(arabic_error)) def test_with_code(self): from acme.messages import Error, is_acme_error diff --git a/certbot/achallenges.py b/certbot/achallenges.py index c2af45fdb..f39bb4cec 100644 --- a/certbot/achallenges.py +++ b/certbot/achallenges.py @@ -28,7 +28,6 @@ logger = logging.getLogger(__name__) # pylint: disable=too-few-public-methods - class AnnotatedChallenge(jose.ImmutableMap): """Client annotated challenge. diff --git a/certbot/tests/errors_test.py b/certbot/tests/errors_test.py index aee1857a6..c8a6c4ac5 100644 --- a/certbot/tests/errors_test.py +++ b/certbot/tests/errors_test.py @@ -23,6 +23,17 @@ class FailedChallengesTest(unittest.TestCase): self.assertTrue(str(self.error).startswith( "Failed authorization procedure. example.com (dns-01): tls")) + def test_unicode(self): + from certbot.errors import FailedChallenges + arabic_detail = u'\u0639\u062f\u0627\u0644\u0629' + arabic_error = FailedChallenges(set([achallenges.DNS( + domain="example.com", challb=messages.ChallengeBody( + chall=acme_util.DNS01, uri=None, + error=messages.Error(typ="tls", detail=arabic_detail)))])) + + self.assertTrue(str(arabic_error).startswith( + "Failed authorization procedure. example.com (dns-01): tls")) + class StandaloneBindErrorTest(unittest.TestCase): """Tests for certbot.errors.StandaloneBindError.""" From a7a8e060e37fad21a9702ac0f143fd39609c8c2d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 6 Jul 2017 13:50:13 -0400 Subject: [PATCH 113/125] Finish adding configurable log rotation * Update log backupCount name and description. * Add additional error handling to --log-backups * test --log-backups flag * Pass log_backups value to RotatingFileHandler * Test that log_backups is properly used * add _test_success_common * Add test_success_with_rollover * Add test_success_without_rollover * mock stderr in cli tests * Set log_backups in PostArgParseSetupTest * Rename "log backups" to "max log backups" --- certbot/cli.py | 38 +++++++++++++++++++++++++++++--------- certbot/log.py | 3 ++- certbot/tests/cli_test.py | 12 ++++++++++++ certbot/tests/log_test.py | 27 ++++++++++++++++++++++----- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index c74a8102f..eaafce762 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -850,15 +850,11 @@ 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( - # Note, 1 is subtracted from max_log_count in certbot/main.py - #> to correct an off by one error. - [None, "paths"], - "--max-log-count", dest="max_log_count", - type=int, default=1000, - help="Specifies the maximum number of certbot log files " - "that will be kept. 0 disables log rotation. 1 causes " - "only the log from the most recent run to be kept. " - "2+ enables log rotation at start of certbot execution.") + None, "--max-log-backups", type=nonnegative_int, default=1000, + help="Specifies the maximum number of backup logs that should " + "be kept by Certbot's built in log rotation. Setting this " + "flag to 0 disables log rotation entirely, causing " + "Certbot to always append to the same log file.") helpful.add( [None, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive", dest="noninteractive_mode", action="store_true", @@ -1385,3 +1381,27 @@ class _RenewHookAction(argparse.Action): raise argparse.ArgumentError( self, "conflicts with --deploy-hook value") namespace.renew_hook = values + + +def nonnegative_int(value): + """Converts value to an int and checks that it is not negative. + + This function should used as the type parameter for argparse + arguments. + + :param str value: value provided on the command line + + :returns: integer representation of value + :rtype: int + + :raises argparse.ArgumentTypeError: if value isn't a non-negative integer + + """ + try: + int_value = int(value) + except ValueError: + raise argparse.ArgumentTypeError("value must be an integer") + + if int_value < 0: + raise argparse.ArgumentTypeError("value must be non-negative") + return int_value diff --git a/certbot/log.py b/certbot/log.py index 889b5c50a..73b2e354f 100644 --- a/certbot/log.py +++ b/certbot/log.py @@ -138,7 +138,8 @@ 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=1000) + log_file_path, maxBytes=2 ** 20, + backupCount=config.max_log_backups) except IOError as error: raise errors.Error(util.PERM_ERR_FMT.format(error)) # rotate on each invocation, rollover only possible when maxBytes diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index cad04f88e..108e07eb3 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -364,6 +364,18 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(namespace.deploy_hook, None) self.assertEqual(namespace.renew_hook, value) + def test_max_log_backups_error(self): + with mock.patch('certbot.cli.sys.stderr'): + self.assertRaises( + SystemExit, self.parse, "--max-log-backups foo".split()) + self.assertRaises( + SystemExit, self.parse, "--max-log-backups -42".split()) + + def test_max_log_backups_success(self): + value = "42" + namespace = self.parse(["--max-log-backups", value]) + self.assertEqual(namespace.max_log_backups, int(value)) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" diff --git a/certbot/tests/log_test.py b/certbot/tests/log_test.py index 72ff076dd..5ee9ad812 100644 --- a/certbot/tests/log_test.py +++ b/certbot/tests/log_test.py @@ -66,8 +66,8 @@ class PostArgParseSetupTest(test_util.TempDirTestCase): def setUp(self): super(PostArgParseSetupTest, self).setUp() self.config = mock.MagicMock( - debug=False, logs_dir=self.tempdir, quiet=False, - verbose_count=constants.CLI_DEFAULTS['verbose_count']) + debug=False, logs_dir=self.tempdir, max_log_backups=1000, + quiet=False, verbose_count=constants.CLI_DEFAULTS['verbose_count']) self.devnull = open(os.devnull, 'w') from certbot.log import ColoredStreamHandler @@ -129,7 +129,7 @@ class SetupLogFileHandlerTest(test_util.TempDirTestCase): def setUp(self): super(SetupLogFileHandlerTest, self).setUp() - self.config = mock.MagicMock(logs_dir=self.tempdir) + self.config = mock.MagicMock(logs_dir=self.tempdir, max_log_backups=42) @mock.patch('certbot.main.logging.handlers.RotatingFileHandler') def test_failure(self, mock_handler): @@ -142,15 +142,32 @@ class SetupLogFileHandlerTest(test_util.TempDirTestCase): else: # pragma: no cover self.fail('Error not raised.') - def test_success(self): + def test_success_with_rollover(self): + self._test_success_common(should_rollover=True) + + def test_success_without_rollover(self): + self.config.max_log_backups = 0 + self._test_success_common(should_rollover=False) + + def _test_success_common(self, should_rollover): log_file = 'test.log' handler, log_path = self._call(self.config, log_file, '%(message)s') + handler.close() + self.assertEqual(handler.level, logging.DEBUG) self.assertEqual(handler.formatter.converter, time.gmtime) expected_path = os.path.join(self.config.logs_dir, log_file) self.assertEqual(log_path, expected_path) - handler.close() + + backup_path = os.path.join(self.config.logs_dir, log_file + '.1') + self.assertEqual(os.path.exists(backup_path), should_rollover) + + @mock.patch('certbot.log.logging.handlers.RotatingFileHandler') + def test_max_log_backups_used(self, mock_handler): + self._call(self.config, 'test.log', '%(message)s') + backup_count = mock_handler.call_args[1]['backupCount'] + self.assertEqual(self.config.max_log_backups, backup_count) class ColoredStreamHandlerTest(unittest.TestCase): From 331d12ed50aa265e2fc7a865a0d8872e9666e9b9 Mon Sep 17 00:00:00 2001 From: r5d Date: Thu, 13 Jul 2017 13:13:59 -0400 Subject: [PATCH 114/125] certbot: Update storage.get_link_target (#4750) (#4923) * certbot: Update storage.get_link_target (#4750) * The `get_link_target` function raises `errors.CertStorageError` when link does not exists. * certbot: Fix typo in storage.get_link_target. --- certbot/storage.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/certbot/storage.py b/certbot/storage.py index 4f167d4ea..d03052dae 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -186,8 +186,15 @@ def get_link_target(link): :returns: Absolute path to the target of link :rtype: str + :raises .CertStorageError: If link does not exists. + """ - target = os.readlink(link) + try: + target = os.readlink(link) + except OSError: + raise errors.CertStorageError( + "Expected {0} to be a symlink".format(link)) + if not os.path.isabs(target): target = os.path.join(os.path.dirname(link), target) return os.path.abspath(target) From 7ed2e91cc781cb0b55df35c2fdd5df00d1f5ac68 Mon Sep 17 00:00:00 2001 From: jonasbn Date: Fri, 14 Jul 2017 12:52:25 +0200 Subject: [PATCH 115/125] Removed null.rst as by request --- docs/api/plugins/null.rst | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 docs/api/plugins/null.rst diff --git a/docs/api/plugins/null.rst b/docs/api/plugins/null.rst deleted file mode 100644 index 97df40987..000000000 --- a/docs/api/plugins/null.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.plugins.null` -------------------------------- - -.. automodule:: certbot.plugins.null - :members: From 0f30f9e96ff346cc88b7217f9e236f25aa727577 Mon Sep 17 00:00:00 2001 From: Baime Date: Sat, 15 Jul 2017 15:17:25 +0200 Subject: [PATCH 116/125] Added test to check revoke cert by key mismatch --- certbot/tests/main_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 25f8d1631..c86e53834 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1026,6 +1026,12 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me jose.ComparableX509(cert), mock.ANY) + def test_revoke_with_key_mismatch(self): + server = 'foo.bar' + self.assertRaises(errors.Error, self._call_no_clientmock, + ['--cert-path', CERT, '--key-path', KEY, + '--server', server, 'revoke']) + @mock.patch('certbot.main._determine_account') def test_revoke_without_key(self, mock_determine_account): mock_determine_account.return_value = (mock.MagicMock(), None) From a8b5dfc76c13ab486f335c8277196a6c33952417 Mon Sep 17 00:00:00 2001 From: Chris J Date: Sat, 15 Jul 2017 00:28:25 -0400 Subject: [PATCH 117/125] Enhancement #4893. More copypastable output, including private key location on disk --- certbot/main.py | 23 +++++++++++++++-------- certbot/tests/main_test.py | 5 ++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index f7421d75e..6964e9352 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -293,11 +293,12 @@ def _find_domains_or_certname(config, installer): return domains, certname -def _report_new_cert(config, cert_path, fullchain_path): +def _report_new_cert(config, cert_path, fullchain_path, key_path=None): """Reports the creation of a new certificate to the user. :param str cert_path: path to cert :param str fullchain_path: path to full chain + :param str key_path: path to private key, if available """ if config.dry_run: @@ -312,13 +313,17 @@ def _report_new_cert(config, cert_path, fullchain_path): # (Nginx and Apache2.4) will want. verbswitch = ' with the "certonly" option' if config.verb == "run" else "" + privkey_statement = 'Your key file has been saved at:{br}{0}{br}'.format( + key_path, br=os.linesep) if key_path else "" # XXX Perhaps one day we could detect the presence of known old webservers # and say something more informative here. - msg = ('Congratulations! Your certificate and chain have been saved at {0}.' - ' Your cert will expire on {1}. To obtain a new or tweaked version of this ' - 'certificate in the future, simply run {2} again{3}. ' - 'To non-interactively renew *all* of your certificates, run "{2} renew"' - .format(fullchain_path, expiry, cli.cli_command, verbswitch)) + msg = ('Congratulations! Your certificate and chain have been saved at:{br}' + '{0}{br}{1}' + 'Your cert will expire on {2}. To obtain a new or tweaked version of this ' + 'certificate in the future, simply run {3} again{4}. ' + 'To non-interactively renew *all* of your certificates, run "{3} renew"' + .format(fullchain_path, privkey_statement, expiry, cli.cli_command, verbswitch, + br=os.linesep)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) @@ -601,7 +606,8 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals cert_path = new_lineage.cert_path if new_lineage else None fullchain_path = new_lineage.fullchain_path if new_lineage else None - _report_new_cert(config, cert_path, fullchain_path) + key_path = new_lineage.key_path if new_lineage else None + _report_new_cert(config, cert_path, fullchain_path, key_path) _install_cert(config, le_client, domains, new_lineage) @@ -686,7 +692,8 @@ def certonly(config, plugins): cert_path = lineage.cert_path if lineage else None fullchain_path = lineage.fullchain_path if lineage else None - _report_new_cert(config, cert_path, fullchain_path) + key_path = lineage.key_path if lineage else None + _report_new_cert(config, cert_path, fullchain_path, key_path) _suggest_donation_if_appropriate(config) def renew(config, unused_plugins): diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 7c2016178..99e5ba0ee 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -678,11 +678,12 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me @test_util.patch_get_utility() def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): cert_path = '/etc/letsencrypt/live/foo.bar' + key_path = '/etc/letsencrypt/live/baz.qux' date = '1970-01-01' mock_notAfter().date.return_value = date mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path, - fullchain_path=cert_path) + fullchain_path=cert_path, key_path=key_path) mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = mock_lineage self._certonly_new_request_common(mock_client) @@ -691,6 +692,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me 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(key_path in cert_msg) self.assertTrue( 'donate' in mock_get_utility().add_message.call_args[0][0]) @@ -1000,6 +1002,7 @@ class MainTest(test_util.TempDirTestCase): # pylint: disable=too-many-public-me mock_get_utility = self._test_certonly_csr_common() cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] self.assertTrue('fullchain.pem' in cert_msg) + self.assertFalse('Your key file has been saved at' in cert_msg) self.assertTrue( 'donate' in mock_get_utility().add_message.call_args[0][0]) From f5580598cda39b9e93393dd618a91accbd7469d2 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 17 Jul 2017 13:08:52 -0700 Subject: [PATCH 118/125] add disable pip version check flag --- certbot-auto | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot-auto b/certbot-auto index 599538891..4c923dbf9 100755 --- a/certbot-auto +++ b/certbot-auto @@ -562,7 +562,7 @@ BootstrapMac() { if ! hash virtualenv 2>/dev/null; then say "virtualenv not installed." say "Installing with pip..." - pip install virtualenv + pip install --disable-pip-version-check virtualenv fi } @@ -1045,9 +1045,9 @@ UNLIKELY_EOF PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e if [ "$VERBOSE" = 1 ]; then - "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + "$VENV_BIN/pip" install --disable-pip-version-check --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` + PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` fi PIP_STATUS=$? set -e From d962b2605a5f00f917e0ceb036dd9a07fbca9434 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 17 Jul 2017 15:27:48 -0700 Subject: [PATCH 119/125] add deprecation warning --- certbot-auto | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/certbot-auto b/certbot-auto index 599538891..099cb46db 100755 --- a/certbot-auto +++ b/certbot-auto @@ -200,6 +200,24 @@ ExperimentalBootstrap() { fi } +DeprecationBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + BootstrapMessage $1 + $2 + fi + else + error "WARNING: certbot-auto support for this $1 is DEPRECATED!" + error "Please visit certbot.eff.org to learn how to download a version of" + error "Certbot that is packaged for your system. While an existing version" + error "of certbot-auto may work currently, we have stopped supporting updating" + error "system packages for your system. Please switch to a packaged version" + error "as soon as possible." + exit 1 + fi +} + DeterminePythonVersion() { for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do # Break (while keeping the LE_PYTHON value) if found. @@ -630,11 +648,11 @@ Bootstrap() { elif [ -f /etc/manjaro-release ]; then ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon elif [ -f /etc/gentoo-release ]; then - ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + DeprecationBootstrap "Gentoo" BootstrapGentooCommon elif uname | grep -iq FreeBSD ; then - ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + DeprecationBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then - ExperimentalBootstrap "macOS" BootstrapMac + DeprecationBootstrap "macOS" BootstrapMac elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then From d327c1c28fe91d9b666f65ae8ef7887cf1778dbc Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 18 Jul 2017 12:47:10 -0700 Subject: [PATCH 120/125] Tweak non-root error message. (#4944) For most people, the right answer will be "run as root," so we should emphasize that over the "how to run as non-root" instructions. --- certbot/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/util.py b/certbot/util.py index a95ef62b9..98424b496 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -48,7 +48,7 @@ ANSI_SGR_RESET = "\033[0m" PERM_ERR_FMT = os.linesep.join(( "The following error was encountered:", "{0}", - "If running as non-root, set --config-dir, " + "Either run as root, or set --config-dir, " "--work-dir, and --logs-dir to writeable paths.")) From 278194fe6d40222ebca466a9a5d04b942752eb7c Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 18 Jul 2017 15:50:23 -0700 Subject: [PATCH 121/125] add log rotation info (#4942) --- docs/using.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 20c24a138..c545dc48b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -786,6 +786,18 @@ By default, the following locations are searched: .. keep it up to date with constants.py +.. _log-rotation: + +Log Rotation +============ + +By default certbot stores status logs in ``/var/log/letsencrypt``. By default +certbot will begin rotating logs once there are 1000 logs in the log directory. +Meaning that once 1000 files are in ``/var/log/letsencrypt`` Certbot will delete +the oldest one to make room for new logs. The number of subsequent logs can be +changed by passing the desired number to the command line flag +``--max-log-backups``. + .. _command-line: Certbot command-line options From b3004fe6cf0abc6449a93e8783763d49119ff735 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 18 Jul 2017 15:57:00 -0700 Subject: [PATCH 122/125] Handle critical SAN extension. (#4931) * Handle critical SAN extension. * Add testdata/critical-san.pem. --- acme/acme/crypto_util.py | 2 +- acme/acme/crypto_util_test.py | 5 +++++ acme/acme/testdata/critical-san.pem | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 acme/acme/testdata/critical-san.pem diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 84b70e4a6..de15284c0 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -218,7 +218,7 @@ def _pyopenssl_cert_or_req_san(cert_or_req): text = func(OpenSSL.crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8") # WARNING: this function does not support multiple SANs extensions. # Multiple X509v3 extensions of the same type is disallowed by RFC 5280. - match = re.search(r"X509v3 Subject Alternative Name:\s*(.*)", text) + match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text) # WARNING: this function assumes that no SAN can include # parts_separator, hence the split! sans_parts = [] if match is None else match.group(1).split(parts_separator) diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 845f43914..4046aa197 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -131,6 +131,11 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase): self.assertEqual(self._call_csr('csr-idnsans.pem'), self._get_idn_names()) + def test_critical_san(self): + self.assertEqual(self._call_cert('critical-san.pem'), + ['chicago-cubs.venafi.example', 'cubs.venafi.example']) + + class RandomSnTest(unittest.TestCase): """Test for random certificate serial numbers.""" diff --git a/acme/acme/testdata/critical-san.pem b/acme/acme/testdata/critical-san.pem new file mode 100644 index 000000000..7aec8ab1c --- /dev/null +++ b/acme/acme/testdata/critical-san.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG +A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5 +MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g +U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN +MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M +0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV +EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU +0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl +XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF +fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD +VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl +bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV +HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo +dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw +SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0 +dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ +KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv +DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w +DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L +ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs +OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst +bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS +yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be +n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv +KA== +-----END CERTIFICATE----- \ No newline at end of file From fa74a32245528dcc81685edad9cb99043120a6b6 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 18 Jul 2017 16:27:33 -0700 Subject: [PATCH 123/125] updated letsencrypt-auto.template --- certbot-auto | 24 +++--------------- letsencrypt-auto-source/letsencrypt-auto | 25 ++++++++++++++++--- .../letsencrypt-auto.template | 25 ++++++++++++++++--- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/certbot-auto b/certbot-auto index 099cb46db..599538891 100755 --- a/certbot-auto +++ b/certbot-auto @@ -200,24 +200,6 @@ ExperimentalBootstrap() { fi } -DeprecationBootstrap() { - # Arguments: Platform name, bootstrap function name - if [ "$DEBUG" = 1 ]; then - if [ "$2" != "" ]; then - BootstrapMessage $1 - $2 - fi - else - error "WARNING: certbot-auto support for this $1 is DEPRECATED!" - error "Please visit certbot.eff.org to learn how to download a version of" - error "Certbot that is packaged for your system. While an existing version" - error "of certbot-auto may work currently, we have stopped supporting updating" - error "system packages for your system. Please switch to a packaged version" - error "as soon as possible." - exit 1 - fi -} - DeterminePythonVersion() { for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do # Break (while keeping the LE_PYTHON value) if found. @@ -648,11 +630,11 @@ Bootstrap() { elif [ -f /etc/manjaro-release ]; then ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon elif [ -f /etc/gentoo-release ]; then - DeprecationBootstrap "Gentoo" BootstrapGentooCommon + ExperimentalBootstrap "Gentoo" BootstrapGentooCommon elif uname | grep -iq FreeBSD ; then - DeprecationBootstrap "FreeBSD" BootstrapFreeBsd + ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then - DeprecationBootstrap "macOS" BootstrapMac + ExperimentalBootstrap "macOS" BootstrapMac elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 276d68c22..1bcc72914 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -200,6 +200,25 @@ ExperimentalBootstrap() { fi } +DeprecationBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + BootstrapMessage $1 + $2 + fi + else + error "WARNING: certbot-auto support for this $1 is DEPRECATED!" + error "Please visit certbot.eff.org to learn how to download a version of" + error "Certbot that is packaged for your system. While an existing version" + error "of certbot-auto may work currently, we have stopped supporting updating" + error "system packages for your system. Please switch to a packaged version" + error "as soon as possible." + exit 1 + fi +} + + DeterminePythonVersion() { for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do # Break (while keeping the LE_PYTHON value) if found. @@ -630,11 +649,11 @@ Bootstrap() { elif [ -f /etc/manjaro-release ]; then ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon elif [ -f /etc/gentoo-release ]; then - ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + DeprecationBootstrap "Gentoo" BootstrapGentooCommon elif uname | grep -iq FreeBSD ; then - ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + DeprecationBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then - ExperimentalBootstrap "macOS" BootstrapMac + DeprecationBootstrap "macOS" BootstrapMac elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 305435a9b..b07bc38e4 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -200,6 +200,25 @@ ExperimentalBootstrap() { fi } +DeprecationBootstrap() { + # Arguments: Platform name, bootstrap function name + if [ "$DEBUG" = 1 ]; then + if [ "$2" != "" ]; then + BootstrapMessage $1 + $2 + fi + else + error "WARNING: certbot-auto support for this $1 is DEPRECATED!" + error "Please visit certbot.eff.org to learn how to download a version of" + error "Certbot that is packaged for your system. While an existing version" + error "of certbot-auto may work currently, we have stopped supporting updating" + error "system packages for your system. Please switch to a packaged version" + error "as soon as possible." + exit 1 + fi +} + + DeterminePythonVersion() { for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do # Break (while keeping the LE_PYTHON value) if found. @@ -260,11 +279,11 @@ Bootstrap() { elif [ -f /etc/manjaro-release ]; then ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon elif [ -f /etc/gentoo-release ]; then - ExperimentalBootstrap "Gentoo" BootstrapGentooCommon + DeprecationBootstrap "Gentoo" BootstrapGentooCommon elif uname | grep -iq FreeBSD ; then - ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd + DeprecationBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then - ExperimentalBootstrap "macOS" BootstrapMac + DeprecationBootstrap "macOS" BootstrapMac elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then From 1cf8c5a5860b9ba1da16a4e4f60458767c6fa594 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 18 Jul 2017 16:37:03 -0700 Subject: [PATCH 124/125] add changes to template --- certbot-auto | 6 +++--- letsencrypt-auto-source/letsencrypt-auto | 6 +++--- letsencrypt-auto-source/letsencrypt-auto.template | 4 ++-- letsencrypt-auto-source/pieces/bootstrappers/mac.sh | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/certbot-auto b/certbot-auto index 4c923dbf9..599538891 100755 --- a/certbot-auto +++ b/certbot-auto @@ -562,7 +562,7 @@ BootstrapMac() { if ! hash virtualenv 2>/dev/null; then say "virtualenv not installed." say "Installing with pip..." - pip install --disable-pip-version-check virtualenv + pip install virtualenv fi } @@ -1045,9 +1045,9 @@ UNLIKELY_EOF PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e if [ "$VERBOSE" = 1 ]; then - "$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" else - PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + 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 diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 276d68c22..17ee1d509 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -562,7 +562,7 @@ BootstrapMac() { if ! hash virtualenv 2>/dev/null; then say "virtualenv not installed." say "Installing with pip..." - pip install virtualenv + pip install --disable-pip-version-check virtualenv fi } @@ -1045,9 +1045,9 @@ UNLIKELY_EOF PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e if [ "$VERBOSE" = 1 ]; then - "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + "$VENV_BIN/pip" install --disable-pip-version-check --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` + PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` fi PIP_STATUS=$? set -e diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 305435a9b..459b020cf 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -330,9 +330,9 @@ UNLIKELY_EOF PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e if [ "$VERBOSE" = 1 ]; then - "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" + "$VENV_BIN/pip" install --disable-pip-version-check --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` + PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` fi PIP_STATUS=$? set -e diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh index b88e96999..4a7e37c66 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh @@ -39,6 +39,6 @@ BootstrapMac() { if ! hash virtualenv 2>/dev/null; then say "virtualenv not installed." say "Installing with pip..." - pip install virtualenv + pip install --disable-pip-version-check virtualenv fi } From 9878f15966da322708eff8fd93f17dec54ab7e5f Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 19 Jul 2017 11:55:53 -0700 Subject: [PATCH 125/125] leave macos unchanged --- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-auto-source/pieces/bootstrappers/mac.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 17ee1d509..077847e8f 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -562,7 +562,7 @@ BootstrapMac() { if ! hash virtualenv 2>/dev/null; then say "virtualenv not installed." say "Installing with pip..." - pip install --disable-pip-version-check virtualenv + pip install virtualenv fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh index 4a7e37c66..b88e96999 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh @@ -39,6 +39,6 @@ BootstrapMac() { if ! hash virtualenv 2>/dev/null; then say "virtualenv not installed." say "Installing with pip..." - pip install --disable-pip-version-check virtualenv + pip install virtualenv fi }