From 21187b7c1bc7eed2c84e3a5925cfdc32393c1b58 Mon Sep 17 00:00:00 2001 From: Luca Olivetti Date: Fri, 4 Dec 2015 16:10:43 +0100 Subject: [PATCH 001/192] mageia bootstrap --- bootstrap/_mageia_common.sh | 24 ++++++++++++++++++++++++ letsencrypt-auto | 3 +++ 2 files changed, 27 insertions(+) create mode 100755 bootstrap/_mageia_common.sh diff --git a/bootstrap/_mageia_common.sh b/bootstrap/_mageia_common.sh new file mode 100755 index 000000000..9a4606c9d --- /dev/null +++ b/bootstrap/_mageia_common.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# Tested on mageia 5 x86_64 +if ! urpmi --force \ + python \ + libpython-devel \ + python-virtualenv +then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 +fi + +if ! urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts +then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 +fi diff --git a/letsencrypt-auto b/letsencrypt-auto index 44c71883c..13a966a87 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -122,6 +122,9 @@ then if [ -f /etc/debian_version ] ; then echo "Bootstrapping dependencies for Debian-based OSes..." $SUDO $BOOTSTRAP/_deb_common.sh + elif [ -f /etc/mageia-release ] ; then + echo "Bootstrapping dependencies for mageia..." + $SUDO $BOOTSTRAP/_mageia_common.sh elif [ -f /etc/redhat-release ] ; then echo "Bootstrapping dependencies for RedHat-based OSes..." $SUDO $BOOTSTRAP/_rpm_common.sh From 15502bb64e50b75a4a5e46a29179492c1b5c9fda Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 17 Mar 2016 16:14:29 -0700 Subject: [PATCH 002/192] renew implies noninteractive should be a property of config Not a property of the config we change later --- letsencrypt/cli.py | 3 +++ letsencrypt/main.py | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 017e0a62e..d3ffd3441 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -409,6 +409,9 @@ class HelpfulArgumentParser(object): # Do any post-parsing homework here + if self.verb == "renew": + self.noninteractive_mode = True + # we get domains from -d, but also from the webroot map... if parsed_args.webroot_map: for domain in parsed_args.webroot_map.keys(): diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 8d59993df..0148bdeca 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -685,9 +685,6 @@ def main(cli_args=sys.argv[1:]): displayer = display_util.NoninteractiveDisplay(sys.stdout) elif config.text_mode: displayer = display_util.FileDisplay(sys.stdout) - elif config.verb == "renew": - config.noninteractive_mode = True - displayer = display_util.NoninteractiveDisplay(sys.stdout) else: displayer = display_util.NcursesDisplay() zope.component.provideUtility(displayer) From b8ea2c19a38d870556496b980de2472b4e369d32 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Apr 2016 16:57:52 -0700 Subject: [PATCH 003/192] Lintian bug fix --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index f2ad50d45..adcc32a5e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -272,7 +272,7 @@ class HelpfulArgumentParser(object): # Do any post-parsing homework here if self.verb == "renew": - self.noninteractive_mode = True + parsed_args.noninteractive_mode = True # we get domains from -d, but also from the webroot map... if parsed_args.webroot_map: From 0bcc80756d16de6aed3365b0fa3d27c2e2e98855 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Apr 2016 17:10:30 -0700 Subject: [PATCH 004/192] Refactor config.server complexity out of parse_args --- letsencrypt/cli.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8c2cd839a..75c7e116f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -318,24 +318,7 @@ class HelpfulArgumentParser(object): parsed_args.noninteractive_mode = True if parsed_args.staging or parsed_args.dry_run: - if parsed_args.server not in (flag_default("server"), constants.STAGING_URI): - conflicts = ["--staging"] if parsed_args.staging else [] - conflicts += ["--dry-run"] if parsed_args.dry_run else [] - raise errors.Error("--server value conflicts with {0}".format( - " and ".join(conflicts))) - - parsed_args.server = constants.STAGING_URI - - if parsed_args.dry_run: - if self.verb not in ["certonly", "renew"]: - raise errors.Error("--dry-run currently only works with the " - "'certonly' or 'renew' subcommands (%r)" % self.verb) - parsed_args.break_my_certs = parsed_args.staging = True - if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")): - # The user has a prod account, but might not have a staging - # one; we don't want to start trying to perform interactive registration - parsed_args.tos = True - parsed_args.register_unsafely_without_email = True + self.set_test_server(parsed_args) if parsed_args.csr: if parsed_args.allow_subset_of_names: @@ -347,6 +330,30 @@ class HelpfulArgumentParser(object): return parsed_args + + def set_test_server(self, parsed_args): + "We have --staging/--dry-run; perform sanity check and set config.server" + + if parsed_args.server not in (flag_default("server"), constants.STAGING_URI): + conflicts = ["--staging"] if parsed_args.staging else [] + conflicts += ["--dry-run"] if parsed_args.dry_run else [] + raise errors.Error("--server value conflicts with {0}".format( + " and ".join(conflicts))) + + parsed_args.server = constants.STAGING_URI + + if parsed_args.dry_run: + if self.verb not in ["certonly", "renew"]: + raise errors.Error("--dry-run currently only works with the " + "'certonly' or 'renew' subcommands (%r)" % self.verb) + parsed_args.break_my_certs = parsed_args.staging = True + if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")): + # The user has a prod account, but might not have a staging + # one; we don't want to start trying to perform interactive registration + parsed_args.tos = True + parsed_args.register_unsafely_without_email = True + + def handle_csr(self, parsed_args): """Process a --csr flag.""" if parsed_args.verb != "certonly": From 5e3fc3a957eaaff225e21916b1241cd5a37ec522 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Apr 2016 17:14:29 -0700 Subject: [PATCH 005/192] Keep all --csr checks in HelpfulArgumentParser.handle_csr --- letsencrypt/cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 75c7e116f..74c51f5e3 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -321,9 +321,6 @@ class HelpfulArgumentParser(object): self.set_test_server(parsed_args) if parsed_args.csr: - if parsed_args.allow_subset_of_names: - raise errors.Error("--allow-subset-of-names " - "cannot be used with --csr") self.handle_csr(parsed_args) hooks.validate_hooks(parsed_args) @@ -361,6 +358,8 @@ class HelpfulArgumentParser(object): "when obtaining a new or replacement " "via the certonly command. Please try the " "certonly command instead.") + if parsed_args.allow_subset_of_names: + raise errors.Error("--allow-subset-of-names cannot be used with --csr") try: csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") From 526bc5cf84aeaca0669d87ada3ced32615efe8e9 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Apr 2016 17:35:29 -0700 Subject: [PATCH 006/192] Refactor CSR importing from cli -> crypto_util More specifically: HelpfulArgumentParser.handle_csr -> crypto_util.import_csr_file --- letsencrypt/cli.py | 18 ++---------------- letsencrypt/crypto_util.py | 30 ++++++++++++++++++++++++++++++ letsencrypt/tests/client_test.py | 6 ++++-- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 74c51f5e3..61fc57777 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -6,10 +6,8 @@ import logging import logging.handlers import os import sys -import traceback import configargparse -import OpenSSL import six import letsencrypt @@ -361,20 +359,8 @@ class HelpfulArgumentParser(object): if parsed_args.allow_subset_of_names: raise errors.Error("--allow-subset-of-names cannot be used with --csr") - try: - csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") - typ = OpenSSL.crypto.FILETYPE_ASN1 - domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - except OpenSSL.crypto.Error: - try: - e1 = traceback.format_exc() - typ = OpenSSL.crypto.FILETYPE_PEM - csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem") - domains = crypto_util.get_sans_from_csr(csr.data, typ) - except OpenSSL.crypto.Error: - logger.debug("DER CSR parse error %s", e1) - logger.debug("PEM CSR parse error %s", traceback.format_exc()) - raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0])) + csrfile, contents = parsed_args.csr[0:2] + typ, csr, domains = crypto_util.import_csr_file(csrfile, contents) # This is not necessary for webroot to work, however, # obtain_certificate_from_csr requires parsed_args.domains to be set diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 5fdcba843..2ca43f76f 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -6,6 +6,7 @@ """ import logging import os +import traceback import OpenSSL import pyrfc3339 @@ -171,6 +172,35 @@ def csr_matches_pubkey(csr, privkey): return False +def import_csr_file(csrfile, contents): + """Import a CSR file, which can be either PEM or DER. + + :param str csrfile: CSR filename + :param str contents: contens of the CSR file + + :rtype: tuple + + :returns: (le_util.CSR object representing the CSR, + OpenSSL FILETYPE_ representing DER or PEM, + list of domains requested in the CSR) + """ + try: + csr = le_util.CSR(file=csrfile, data=contents, form="der") + typ = OpenSSL.crypto.FILETYPE_ASN1 + domains = get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) + except OpenSSL.crypto.Error: + try: + e1 = traceback.format_exc() + typ = OpenSSL.crypto.FILETYPE_PEM + csr = le_util.CSR(file=csrfile, data=contents, form="pem") + domains = get_sans_from_csr(csr.data, typ) + except OpenSSL.crypto.Error: + logger.debug("DER CSR parse error %s", e1) + logger.debug("PEM CSR parse error %s", traceback.format_exc()) + raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) + return typ, csr, domains + + def make_key(bits): """Generate PEM encoded RSA key. diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index cd6b11158..c3d5b322f 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -134,7 +134,7 @@ class ClientTest(unittest.TestCase): self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) - # FIXME move parts of this to test_cli.py... + # FIXME move parts of this to crypto_util tests... @mock.patch("letsencrypt.client.logger") def test_obtain_certificate_from_csr(self, mock_logger): self._mock_obtain_certificate() @@ -144,9 +144,11 @@ class ClientTest(unittest.TestCase): # The CLI should believe that this is a certonly request, because # a CSR would not be allowed with other kinds of requests! mock_parsed_args.verb = "certonly" - with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: + with mock.patch("letsencrypt.cli.crypto_util.le_util.CSR") as mock_CSR: mock_CSR.return_value = test_csr mock_parsed_args.domains = self.eg_domains[:] + mock_parsed_args.allow_subset_of_names = False + mock_parsed_args.csr = (mock.MagicMock(), mock.MagicMock()) mock_parser = mock.MagicMock(cli.HelpfulArgumentParser) cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args) From c82a551e77faa228da0a9e445cd0eb7f0417537d Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 00:03:48 +0300 Subject: [PATCH 007/192] os-release parsing WIP --- letsencrypt/le_util.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index cb1c61074..7ee5e33db 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -210,8 +210,56 @@ def safely_remove(path): def get_os_info(): + """ + Get OS name and version + + :returns: (os_name, os_version) + :rtype: `tuple` of `str` + """ + + +def get_systemd_os_info(): + """ + Parse systemd /etc/os-release for distribution information + + :returns: (os_name, os_version) + :rtype: `tuple` of `str` + """ + + os_name = _get_systemd_os_release_var("ID") + os_version = _get_systemd_os_release_var("VERSION_ID") + + return (os_name, os_version) + + +def _get_systemd_os_release_var(varname): + + OS_RELEASE_FILEPATH = "/etc/os-release" + var_string = varname+"=" + if not os.path.isfile(OS_RELEASE_FILEPATH): + return "" + with open(OS_RELEASE_FILEPATH, 'r') as fh: + contents = fh.readlines() + + for line in contents: + if line.strip().startswith(var_string): + # Return the value of var, normalized + return _normalize_string(line.strip()[len(var_string):]) + return "" + + +def _normalize_string(orig): + """ + Helper function for _get_systemd_os_release_var() to remove quotes + and whitespaces + """ + return orig.replace('"', '').replace("'", "").strip() + + +def get_python_os_info(): """ Get Operating System type/distribution and major version + using python platform module :returns: (os_name, os_version) :rtype: `tuple` of `str` From bbb300eb229bcca0938b50fef9421f02b3b6d88b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 14:27:00 +0300 Subject: [PATCH 008/192] Finalized parsing and fixed test case --- letsencrypt/le_util.py | 16 ++++++++++++++++ letsencrypt/tests/cli_test.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 7ee5e33db..60cdd0314 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -217,6 +217,15 @@ def get_os_info(): :rtype: `tuple` of `str` """ + if os.path.isfile('/etc/os-release'): + # Systemd os-release parsing might be viable + os_name, os_version = get_systemd_os_info() + if os_name: + return (os_name, os_version) + + # Fallback to platform module + return get_python_os_info() + def get_systemd_os_info(): """ @@ -233,6 +242,13 @@ def get_systemd_os_info(): def _get_systemd_os_release_var(varname): + """ + Get single value from systemd /etc/os-release + + :param str varname: Name of variable to fetch + :returns: requested value + :rtype: `str` + """ OS_RELEASE_FILEPATH = "/etc/os-release" var_string = varname+"=" diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index eb3f48308..03ad45514 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -169,7 +169,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods import platform plat = platform.platform() if "linux" in plat.lower(): - self.assertTrue(platform.linux_distribution()[0] in ua) + self.assertTrue(le_util.get_os_info()[0] in ua) with mock.patch('letsencrypt.main.client.acme_client.ClientNetwork') as acme_net: ua = "bandersnatch" From 7ff8440b8f49d527f3a61055aa8fb311ca90063b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 18:12:47 +0300 Subject: [PATCH 009/192] Tests for systemd os-release. Fix for darwin OS version info and tests for it --- letsencrypt/le_util.py | 1 - letsencrypt/tests/le_util_test.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 60cdd0314..71dff7575 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -300,7 +300,6 @@ def get_python_os_info(): ["sw_vers", "-productVersion"], stdout=subprocess.PIPE ).communicate()[0] - os_ver = os_ver.partition(".")[0] elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 0f9464c6f..e1770eaed 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -339,5 +339,39 @@ class EnforceDomainSanityTest(unittest.TestCase): u"eichh\u00f6rnchen.example.com") +class OsInfoTest(unittest.TestCase): + """Test OS / distribution detection""" + def _call(self): + from letsencrypt.le_util import get_os_info + return get_os_info() + + def test_systemd_os_release(self): + from letsencrypt.le_util import get_os_info + os_release = 'VERSION_ID=42\nID=doobian\n' + with mock.patch('__builtin__.open', + mock.mock_open(read_data=os_release)): + with mock.patch('os.path.isfile', return_value=True): + self.assertEqual(get_os_info()[0], 'doobian') + self.assertEqual(get_os_info()[1], '42') + + @mock.patch("letsencrypt.le_util.subprocess.Popen") + def test_non_systemd_os_info(self, popen_mock): + from letsencrypt.le_util import get_os_info + with mock.patch('os.path.isfile', return_value=False): + with mock.patch('platform.system_alias', + return_value=('NonSystemD','42','42')): + self.assertEqual(get_os_info()[0], 'nonsystemd') + + with mock.patch('platform.system_alias', + return_value=('darwin', '', '')): + comm_mock = mock.Mock() + comm_attrs = {'communicate.return_value': + ('42.42.42', 'error')} + comm_mock.configure_mock(**comm_attrs) + popen_mock.return_value = comm_mock + self.assertEqual(get_os_info()[0], 'darwin') + self.assertEqual(get_os_info()[1], '42.42.42') + + if __name__ == "__main__": unittest.main() # pragma: no cover From 34f0e260f1ee7c900330d60f47b7e6a06aabd15b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 18:49:08 +0300 Subject: [PATCH 010/192] Linter fixes --- letsencrypt/tests/le_util_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index e1770eaed..10d2c91ad 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -359,7 +359,7 @@ class OsInfoTest(unittest.TestCase): from letsencrypt.le_util import get_os_info with mock.patch('os.path.isfile', return_value=False): with mock.patch('platform.system_alias', - return_value=('NonSystemD','42','42')): + return_value=('NonSystemD', '42', '42')): self.assertEqual(get_os_info()[0], 'nonsystemd') with mock.patch('platform.system_alias', @@ -367,7 +367,7 @@ class OsInfoTest(unittest.TestCase): comm_mock = mock.Mock() comm_attrs = {'communicate.return_value': ('42.42.42', 'error')} - comm_mock.configure_mock(**comm_attrs) + comm_mock.configure_mock(**comm_attrs) # pylint disable=star-args popen_mock.return_value = comm_mock self.assertEqual(get_os_info()[0], 'darwin') self.assertEqual(get_os_info()[1], '42.42.42') From 57738142e28c6841aab9a909883ecf166e6b9560 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 18:59:27 +0300 Subject: [PATCH 011/192] Added constants for os-release names --- letsencrypt-apache/letsencrypt_apache/constants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/letsencrypt-apache/letsencrypt_apache/constants.py b/letsencrypt-apache/letsencrypt_apache/constants.py index 8b502b4d8..72bbdca6d 100644 --- a/letsencrypt-apache/letsencrypt_apache/constants.py +++ b/letsencrypt-apache/letsencrypt_apache/constants.py @@ -78,6 +78,8 @@ CLI_DEFAULTS = { "centos linux": CLI_DEFAULTS_CENTOS, "fedora": CLI_DEFAULTS_CENTOS, "red hat enterprise linux server": CLI_DEFAULTS_CENTOS, + "rhel": CLI_DEFAULTS_CENTOS, + "amazon": CLI_DEFAULTS_CENTOS, "gentoo base system": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, } From 67c60ab406e7b1b34637371e7392d8ce51889d82 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 12 Apr 2016 19:41:39 +0300 Subject: [PATCH 012/192] Disabled linter error --- letsencrypt/tests/le_util_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 10d2c91ad..74b1bb703 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -347,11 +347,11 @@ class OsInfoTest(unittest.TestCase): def test_systemd_os_release(self): from letsencrypt.le_util import get_os_info - os_release = 'VERSION_ID=42\nID=doobian\n' + os_release = 'VERSION_ID=42\nID=systemdos\n' with mock.patch('__builtin__.open', mock.mock_open(read_data=os_release)): with mock.patch('os.path.isfile', return_value=True): - self.assertEqual(get_os_info()[0], 'doobian') + self.assertEqual(get_os_info()[0], 'systemdos') self.assertEqual(get_os_info()[1], '42') @mock.patch("letsencrypt.le_util.subprocess.Popen") @@ -367,7 +367,7 @@ class OsInfoTest(unittest.TestCase): comm_mock = mock.Mock() comm_attrs = {'communicate.return_value': ('42.42.42', 'error')} - comm_mock.configure_mock(**comm_attrs) # pylint disable=star-args + comm_mock.configure_mock(**comm_attrs) # pylint: disable=star-args popen_mock.return_value = comm_mock self.assertEqual(get_os_info()[0], 'darwin') self.assertEqual(get_os_info()[1], '42.42.42') From 6fc63de5a587c91f3ba87d51b4e38ef8d9f0b44a Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Wed, 13 Apr 2016 00:49:51 +0300 Subject: [PATCH 013/192] Using mocked os-release file --- letsencrypt/le_util.py | 22 ++++++++++++---------- letsencrypt/tests/le_util_test.py | 13 +++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 71dff7575..927d8f2e8 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -209,17 +209,18 @@ def safely_remove(path): raise -def get_os_info(): +def get_os_info(filepath="/etc/os-release"): """ Get OS name and version + :param str filepath: File path of os-release file :returns: (os_name, os_version) :rtype: `tuple` of `str` """ - if os.path.isfile('/etc/os-release'): + if os.path.isfile(filepath): # Systemd os-release parsing might be viable - os_name, os_version = get_systemd_os_info() + os_name, os_version = get_systemd_os_info(filepath=filepath) if os_name: return (os_name, os_version) @@ -227,34 +228,35 @@ def get_os_info(): return get_python_os_info() -def get_systemd_os_info(): +def get_systemd_os_info(filepath="/etc/os-release"): """ Parse systemd /etc/os-release for distribution information + :param str filepath: File path of os-release file :returns: (os_name, os_version) :rtype: `tuple` of `str` """ - os_name = _get_systemd_os_release_var("ID") - os_version = _get_systemd_os_release_var("VERSION_ID") + os_name = _get_systemd_os_release_var("ID", filepath=filepath) + os_version = _get_systemd_os_release_var("VERSION_ID", filepath=filepath) return (os_name, os_version) -def _get_systemd_os_release_var(varname): +def _get_systemd_os_release_var(varname, filepath="/etc/os-release"): """ Get single value from systemd /etc/os-release :param str varname: Name of variable to fetch + :param str filepath: File path of os-release file :returns: requested value :rtype: `str` """ - OS_RELEASE_FILEPATH = "/etc/os-release" var_string = varname+"=" - if not os.path.isfile(OS_RELEASE_FILEPATH): + if not os.path.isfile(filepath): return "" - with open(OS_RELEASE_FILEPATH, 'r') as fh: + with open(filepath, 'r') as fh: contents = fh.readlines() for line in contents: diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 74b1bb703..c43dbfe2c 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -4,6 +4,7 @@ import errno import os import shutil import stat +import sys import tempfile import unittest @@ -11,6 +12,7 @@ import mock import six from letsencrypt import errors +from letsencrypt.tests import test_util class RunScriptTest(unittest.TestCase): @@ -347,12 +349,11 @@ class OsInfoTest(unittest.TestCase): def test_systemd_os_release(self): from letsencrypt.le_util import get_os_info - os_release = 'VERSION_ID=42\nID=systemdos\n' - with mock.patch('__builtin__.open', - mock.mock_open(read_data=os_release)): - with mock.patch('os.path.isfile', return_value=True): - self.assertEqual(get_os_info()[0], 'systemdos') - self.assertEqual(get_os_info()[1], '42') + with mock.patch('os.path.isfile', return_value=True): + self.assertEqual(get_os_info( + test_util.vector_path("os-release"))[0], 'systemdos') + self.assertEqual(get_os_info( + test_util.vector_path("os-release"))[1], '42') @mock.patch("letsencrypt.le_util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): From 608352157c3fb618e4e345509e0163481596dcba Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Wed, 13 Apr 2016 00:55:21 +0300 Subject: [PATCH 014/192] ..and the test file of course --- letsencrypt/tests/testdata/os-release | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 letsencrypt/tests/testdata/os-release diff --git a/letsencrypt/tests/testdata/os-release b/letsencrypt/tests/testdata/os-release new file mode 100644 index 000000000..b7c3ceb1b --- /dev/null +++ b/letsencrypt/tests/testdata/os-release @@ -0,0 +1,8 @@ +NAME="SystemdOS" +VERSION="42.42.42 LTS, Unreal" +ID=systemdos +ID_LIKE=debian +PRETTY_NAME="SystemdOS 42.42.42 Unreal" +VERSION_ID="42" +HOME_URL="http://www.example.com/" +SUPPORT_URL="http://help.example.com/" From 096873ca1c7d2c998283fa0cd3835db6bac3ea9c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Wed, 13 Apr 2016 01:21:34 +0300 Subject: [PATCH 015/192] Removed unused import --- letsencrypt/tests/le_util_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index c43dbfe2c..bab711ded 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -4,7 +4,6 @@ import errno import os import shutil import stat -import sys import tempfile import unittest From 995b22684f20a5de2f5b48dc34816f54f9af654c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 14 Apr 2016 08:08:08 +0300 Subject: [PATCH 016/192] Removed unused test method --- letsencrypt/tests/le_util_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index bab711ded..5187964a4 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -342,9 +342,6 @@ class EnforceDomainSanityTest(unittest.TestCase): class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" - def _call(self): - from letsencrypt.le_util import get_os_info - return get_os_info() def test_systemd_os_release(self): from letsencrypt.le_util import get_os_info From b1d7bd318e649474dd763667920e8e8744b961b9 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 14 Apr 2016 08:39:39 +0300 Subject: [PATCH 017/192] Full test coverage for le_util and os detection --- letsencrypt/tests/le_util_test.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 5187964a4..435760828 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -344,16 +344,19 @@ class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" def test_systemd_os_release(self): - from letsencrypt.le_util import get_os_info + from letsencrypt.le_util import get_os_info, get_systemd_os_info with mock.patch('os.path.isfile', return_value=True): self.assertEqual(get_os_info( test_util.vector_path("os-release"))[0], 'systemdos') self.assertEqual(get_os_info( test_util.vector_path("os-release"))[1], '42') + self.assertEqual(get_systemd_os_info("/dev/null"), ("", "")) + with mock.patch('os.path.isfile', return_value=False): + self.assertEqual(get_systemd_os_info(), ("", "")) @mock.patch("letsencrypt.le_util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): - from letsencrypt.le_util import get_os_info + from letsencrypt.le_util import get_os_info, get_python_os_info with mock.patch('os.path.isfile', return_value=False): with mock.patch('platform.system_alias', return_value=('NonSystemD', '42', '42')): @@ -364,11 +367,32 @@ class OsInfoTest(unittest.TestCase): comm_mock = mock.Mock() comm_attrs = {'communicate.return_value': ('42.42.42', 'error')} - comm_mock.configure_mock(**comm_attrs) # pylint: disable=star-args + comm_mock.configure_mock(**comm_attrs) # pylint: disable=star-args popen_mock.return_value = comm_mock self.assertEqual(get_os_info()[0], 'darwin') self.assertEqual(get_os_info()[1], '42.42.42') + with mock.patch('platform.system_alias', + return_value=('linux', '', '')): + with mock.patch('platform.linux_distribution', + return_value=('', '', '')): + self.assertEqual(get_python_os_info(), ("linux", "")) + + with mock.patch('platform.linux_distribution', + return_value=('testdist', '42', '')): + self.assertEqual(get_python_os_info(), ("testdist", "42")) + + with mock.patch('platform.system_alias', + return_value=('freebsd', '9.3-RC3-p1', '')): + self.assertEqual(get_python_os_info(), ("freebsd", "9")) + + with mock.patch('platform.system_alias', + return_value=('windows', '', '')): + with mock.patch('platform.win32_ver', + return_value=('4242', '95', '2', '')): + self.assertEqual(get_python_os_info(), + ("windows", "95")) + if __name__ == "__main__": unittest.main() # pragma: no cover From 7563d65cb324c5702f55593276e941e036e585b8 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 16 Apr 2016 15:39:31 +0300 Subject: [PATCH 018/192] Name change for tests --- certbot/tests/le_util_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index 752e66a63..23ea40987 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -344,7 +344,7 @@ class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" def test_systemd_os_release(self): - from letsencrypt.le_util import get_os_info, get_systemd_os_info + from certbot.le_util import get_os_info, get_systemd_os_info with mock.patch('os.path.isfile', return_value=True): self.assertEqual(get_os_info( test_util.vector_path("os-release"))[0], 'systemdos') @@ -354,9 +354,9 @@ class OsInfoTest(unittest.TestCase): with mock.patch('os.path.isfile', return_value=False): self.assertEqual(get_systemd_os_info(), ("", "")) - @mock.patch("letsencrypt.le_util.subprocess.Popen") + @mock.patch("certbot.le_util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): - from letsencrypt.le_util import get_os_info, get_python_os_info + from certbot.le_util import get_os_info, get_python_os_info with mock.patch('os.path.isfile', return_value=False): with mock.patch('platform.system_alias', return_value=('NonSystemD', '42', '42')): From c2a5fb7f21c04af7a3664e14b4d7e73af23d6db9 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 16 Apr 2016 16:03:46 +0300 Subject: [PATCH 019/192] Re-add test file --- certbot/tests/testdata/os-release | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 certbot/tests/testdata/os-release diff --git a/certbot/tests/testdata/os-release b/certbot/tests/testdata/os-release new file mode 100644 index 000000000..b7c3ceb1b --- /dev/null +++ b/certbot/tests/testdata/os-release @@ -0,0 +1,8 @@ +NAME="SystemdOS" +VERSION="42.42.42 LTS, Unreal" +ID=systemdos +ID_LIKE=debian +PRETTY_NAME="SystemdOS 42.42.42 Unreal" +VERSION_ID="42" +HOME_URL="http://www.example.com/" +SUPPORT_URL="http://help.example.com/" From ff30fb71d2cabeae6951d3c77ed536fe1070c415 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 29 Apr 2016 15:31:59 +0300 Subject: [PATCH 020/192] New method for determining UA string --- certbot/client.py | 2 +- certbot/le_util.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 60e37a787..4c90a84ca 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -52,7 +52,7 @@ def _determine_user_agent(config): if config.user_agent is None: ua = "CertbotACMEClient/{0} ({1}) Authenticator/{2} Installer/{3}" - ua = ua.format(certbot.__version__, " ".join(le_util.get_os_info()), + ua = ua.format(certbot.__version__, le_util.get_os_info_ua(), config.authenticator, config.installer) else: ua = config.user_agent diff --git a/certbot/le_util.py b/certbot/le_util.py index c1f4d1acd..fcde840c9 100644 --- a/certbot/le_util.py +++ b/certbot/le_util.py @@ -228,6 +228,24 @@ def get_os_info(filepath="/etc/os-release"): return get_python_os_info() +def get_os_info_ua(filepath="/etc/os-release"): + """ + Get OS name and version string for User Agent + + :param str filepath: File path of os-release file + :returns: os_ua + :rtype: `str` + """ + + if os.path.isfile(filepath): + os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + if os_ua: + return os_ua + + # Fallback + return " ".join(get_python_os_info()) + + def get_systemd_os_info(filepath="/etc/os-release"): """ Parse systemd /etc/os-release for distribution information From 1b5efc842719312fbb22cd19f27ab8011ace3f81 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 29 Apr 2016 15:52:24 +0300 Subject: [PATCH 021/192] Added tests for new UA method --- certbot/tests/le_util_test.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index 23ea40987..99aaba44e 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -344,23 +344,31 @@ class OsInfoTest(unittest.TestCase): """Test OS / distribution detection""" def test_systemd_os_release(self): - from certbot.le_util import get_os_info, get_systemd_os_info + from certbot.le_util import (get_os_info, get_systemd_os_info, + get_os_info_ua) + with mock.patch('os.path.isfile', return_value=True): self.assertEqual(get_os_info( test_util.vector_path("os-release"))[0], 'systemdos') self.assertEqual(get_os_info( test_util.vector_path("os-release"))[1], '42') self.assertEqual(get_systemd_os_info("/dev/null"), ("", "")) + self.assertEqual(get_os_info_ua( + test_util.vector_path("os-release")), + "SystemdOS") with mock.patch('os.path.isfile', return_value=False): self.assertEqual(get_systemd_os_info(), ("", "")) @mock.patch("certbot.le_util.subprocess.Popen") def test_non_systemd_os_info(self, popen_mock): - from certbot.le_util import get_os_info, get_python_os_info + from certbot.le_util import (get_os_info, get_python_os_info, + get_os_info_ua) with mock.patch('os.path.isfile', return_value=False): with mock.patch('platform.system_alias', return_value=('NonSystemD', '42', '42')): self.assertEqual(get_os_info()[0], 'nonsystemd') + self.assertEqual(get_os_info_ua(), + " ".join(get_python_os_info())) with mock.patch('platform.system_alias', return_value=('darwin', '', '')): From 9af0994ca6144daa45903e182f48e7036bd9047b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 29 Apr 2016 16:23:52 +0300 Subject: [PATCH 022/192] More verbose UA & test UA test fixes --- certbot/le_util.py | 9 ++++++--- certbot/tests/cli_test.py | 4 ++-- certbot/tests/testdata/os-release | 1 - 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/certbot/le_util.py b/certbot/le_util.py index fcde840c9..d18032d9c 100644 --- a/certbot/le_util.py +++ b/certbot/le_util.py @@ -238,7 +238,9 @@ def get_os_info_ua(filepath="/etc/os-release"): """ if os.path.isfile(filepath): - os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) + os_ua = _get_systemd_os_release_var("PRETTY_NAME", filepath=filepath) + if not os_ua: + os_ua = _get_systemd_os_release_var("NAME", filepath=filepath) if os_ua: return os_ua @@ -396,7 +398,7 @@ def enforce_domain_sanity(domain): domain = domain.encode('ascii').lower() except UnicodeError: error_fmt = (u"Internationalized domain names " - "are not presently supported: {0}") + "are not presently supported: {0}") if isinstance(domain, six.text_type): raise errors.ConfigurationError(error_fmt.format(domain)) else: @@ -423,5 +425,6 @@ def enforce_domain_sanity(domain): # first and last char is not "-" fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Wed, 11 May 2016 10:44:40 -0700 Subject: [PATCH 023/192] Add a way to update registrations Fixes #1215 --- certbot/cli.py | 10 ++++++++-- certbot/main.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 97b1a5399..b4460f681 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -265,8 +265,9 @@ class HelpfulArgumentParser(object): self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert, "config_changes": main.config_changes, "run": main.run, "install": main.install, "plugins": main.plugins_cmd, - "renew": main.renew, "revoke": main.revoke, - "rollback": main.rollback, "everything": main.run} + "register": main.register, "renew": main.renew, + "revoke": main.revoke, "rollback": main.rollback, + "everything": main.run} # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) @@ -591,6 +592,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "certificates. Updates to the Subscriber Agreement will still " "affect you, and will be effective 14 days after posting an " "update to the web site.") + helpful.add( + None, "--update-registration", action="store_true", + help="With the register verb, indicates that details associated " + "with an existing registration, such as the e-mail address, " + "should be updated, rather than registering a new account.") helpful.add(None, "-m", "--email", help=config_help("email")) # positional arg shadows --domains, instead of appending, and # --domains is useful, because it can be stored in config diff --git a/certbot/main.py b/certbot/main.py index 309889e8e..08a5a3ea4 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -10,6 +10,7 @@ import traceback import zope.component +from acme import errors as acme_errors from acme import jose import certbot @@ -363,6 +364,33 @@ def _init_le_client(config, authenticator, installer): return client.Client(config, acc, authenticator, installer, acme=acme) +def register(config, unused_plugins): + """Create or modify accounts on the server.""" + + # Currently, only --update-registration is implemented. Issue #2446 + # calls for a fuller register verb, to allow better separation of + # account management from obtaining certs. + if not config.update_registration: + return "Currently, only register --update-registration is implemented." + if config.email is None: + return ("Currently, --update-registration can only change the e-mail " + "address\nassociated with an account. A new e-mail address is " + "required\n(hint: --email)") + acc, acme = _determine_account(config) + acme_client = client.Client(config, acc, None, None, acme=acme) + try: + updated_reg = client.messages.Registration.from_data(email=config.email) + acme_client.acme.update_registration(acme_client.account.regr, + updated_reg) + except acme_errors.UnexpectedUpdate: + # We expect the unexpected update! + pass + query_data = acme_client.acme.query_registration(acme_client.account.regr) + # We rely on an ACME exception to interrupt this process if it didn't work. + print("Registration change succeeded. New registration data:\n") + print(query_data) + + def install(config, plugins): """Install a previously obtained cert in a server.""" # XXX: Update for renewer/RenewableCert From a7d0b1a7d38111eeea96bf3aaa0eedee46ecc234 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 12 May 2016 17:49:44 -0700 Subject: [PATCH 024/192] Address review comments --- certbot/cli.py | 2 +- certbot/crypto_util.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 796925c1d..430384a5a 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -349,7 +349,7 @@ class HelpfulArgumentParser(object): def set_test_server(self, parsed_args): - "We have --staging/--dry-run; perform sanity check and set config.server" + """We have --staging/--dry-run; perform sanity check and set config.server""" if parsed_args.server not in (flag_default("server"), constants.STAGING_URI): conflicts = ["--staging"] if parsed_args.staging else [] diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index ceec6db71..07e7f9fd2 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -178,11 +178,11 @@ def import_csr_file(csrfile, contents): :param str csrfile: CSR filename :param str contents: contens of the CSR file - :rtype: tuple - :returns: (le_util.CSR object representing the CSR, - OpenSSL FILETYPE_ representing DER or PEM, + `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, list of domains requested in the CSR) + + :rtype: tuple """ try: csr = le_util.CSR(file=csrfile, data=contents, form="der") From f092669347291463047707551a736b21c8200de9 Mon Sep 17 00:00:00 2001 From: sagi Date: Mon, 16 May 2016 21:19:07 +0000 Subject: [PATCH 025/192] If cert_path provided - do not randomize it --- certbot/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 6f41a3a0b..fda7707f6 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -317,7 +317,13 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) + + if cert_path != constants.CLI_DEFAULTS['auth_cert_path']: + cert_file = le_util.safe_open(cert_path, chmod=0o644) + act_cert_path = cert_path + else: + cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) + try: cert_file.write(cert_pem) finally: From c0228ef1aa505a84a87529f76177f7dc6aa51214 Mon Sep 17 00:00:00 2001 From: sagi Date: Mon, 16 May 2016 22:11:15 +0000 Subject: [PATCH 026/192] Boulder integration scripts provides a cert_path --- 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 201343525..a1245e1c9 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -43,7 +43,7 @@ export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ common auth --csr "$CSR_PATH" \ --cert-path "${root}/csr/cert.pem" \ --chain-path "${root}/csr/chain.pem" -openssl x509 -in "${root}/csr/0000_cert.pem" -text +openssl x509 -in "${root}/csr/cert.pem" -text openssl x509 -in "${root}/csr/0000_chain.pem" -text common --domains le3.wtf install \ From 7e3c9399e54f2fb4960296e6211595707ff2dcf5 Mon Sep 17 00:00:00 2001 From: sagi Date: Tue, 17 May 2016 22:12:11 +0000 Subject: [PATCH 027/192] Use cli.set_by_cli to detect if the user explicitly set cert_path --- certbot/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index fda7707f6..dd38e47b0 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -24,6 +24,7 @@ from certbot import interfaces from certbot import le_util from certbot import reverter from certbot import storage +from certbot import cli from certbot.display import ops as display_ops from certbot.display import enhancements @@ -318,7 +319,7 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - if cert_path != constants.CLI_DEFAULTS['auth_cert_path']: + if cli.set_by_cli('cert_path'): cert_file = le_util.safe_open(cert_path, chmod=0o644) act_cert_path = cert_path else: From 12a0312282da5b22d67d0048d2a4bf79739b0a5b Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Thu, 5 May 2016 09:22:50 +0200 Subject: [PATCH 028/192] Fixing auto_test.py for Python 2.6 --- letsencrypt-auto-source/tests/auto_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 3b7e8731b..fa265c1c0 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -11,7 +11,7 @@ from shutil import copy, rmtree import socket import ssl from stat import S_IRUSR, S_IXUSR -from subprocess import CalledProcessError, check_output, Popen, PIPE +from subprocess import CalledProcessError, Popen, PIPE import sys from tempfile import mkdtemp from threading import Thread @@ -146,7 +146,7 @@ def out_and_err(command, input=None, shell=False, env=None): out, err = process.communicate(input=input) status = process.poll() # same as in check_output(), though wait() sounds better if status: - raise CalledProcessError(status, command, output=out) + raise CalledProcessError(status, command) return out, err From d57c9434710a2bd1d74dd382420da6caf9781974 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Thu, 5 May 2016 10:08:28 +0200 Subject: [PATCH 029/192] Fixing broken tests --- letsencrypt-auto-source/tests/auto_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index fa265c1c0..7f0b31b67 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -146,7 +146,9 @@ def out_and_err(command, input=None, shell=False, env=None): out, err = process.communicate(input=input) status = process.poll() # same as in check_output(), though wait() sounds better if status: - raise CalledProcessError(status, command) + error = CalledProcessError(status, command) + error.output = out + raise error return out, err From 50421e99beefd319cc864fa90cd7ba13e9f41786 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 18 May 2016 09:56:34 -0700 Subject: [PATCH 030/192] Factor loading cert/req into its own function --- certbot/crypto_util.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 3f2267af2..41e675471 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -228,15 +228,20 @@ def pyopenssl_load_certificate(data): str(error) for error in openssl_errors))) -def _get_sans_from_cert_or_req(cert_or_req_str, load_func, - typ=OpenSSL.crypto.FILETYPE_PEM): +def _load_cert_or_req(cert_or_req_str, load_func, + typ=OpenSSL.crypto.FILETYPE_PEM): try: - cert_or_req = load_func(typ, cert_or_req_str) + return load_func(typ, cert_or_req_str) except OpenSSL.crypto.Error as error: logger.exception(error) raise + + +def _get_sans_from_cert_or_req(cert_or_req_str, load_func, + typ=OpenSSL.crypto.FILETYPE_PEM): # pylint: disable=protected-access - return acme_crypto_util._pyopenssl_cert_or_req_san(cert_or_req) + return acme_crypto_util._pyopenssl_cert_or_req_san(_load_cert_or_req( + cert_or_req_str, load_func, typ)) def get_sans_from_cert(cert, typ=OpenSSL.crypto.FILETYPE_PEM): From 8e17d7498de484857daead51b56f50b186341233 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 18 May 2016 10:14:15 -0700 Subject: [PATCH 031/192] Add get_names_from_csr --- certbot/crypto_util.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 41e675471..b273cf59f 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -272,6 +272,27 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): csr, OpenSSL.crypto.load_certificate_request, typ) +def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): + """Get a list of domains from a CSR, including the CN if it is set. + + :param str csr: CSR (encoded). + :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` + + :returns: A list of domain names. + :rtype: list + + """ + loaded_csr = _load_cert_or_req( + csr, OpenSSL.crypto.load_certificate_request, typ) + common_name = loaded_csr.get_subject().CN + + # Use a set to avoid duplication with CN and Subject Alt Names + domains = set() if common_name is None else set((common_name,)) + # pylint: disable=protected-access + domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr)) + return list(domains) + + def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. From 77e4be933cbfffaadda97d45e2dfb17672de56b5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 18 May 2016 13:59:17 -0700 Subject: [PATCH 032/192] Simplify get_names_from_csr --- certbot/crypto_util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index b273cf59f..1f87dc816 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -284,10 +284,8 @@ def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """ loaded_csr = _load_cert_or_req( csr, OpenSSL.crypto.load_certificate_request, typ) - common_name = loaded_csr.get_subject().CN - # Use a set to avoid duplication with CN and Subject Alt Names - domains = set() if common_name is None else set((common_name,)) + domains = set(d for d in (loaded_csr.get_subject().CN,) if d is not None) # pylint: disable=protected-access domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr)) return list(domains) From 94549219c593dfc687b01f65ee89dc2287dae06e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 18 May 2016 14:06:32 -0700 Subject: [PATCH 033/192] Add get_names_from_csr tests --- certbot/tests/crypto_util_test.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index ff8d8142e..eade4861f 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -234,6 +234,36 @@ class GetSANsFromCSRTest(unittest.TestCase): [], self._call(test_util.load_vector('csr-nosans.pem'))) +class GetNamesFromCSRTest(unittest.TestCase): + """Tests for certbot.crypto_util.get_names_from_csr.""" + @classmethod + def _call(cls, *args, **kwargs): + from certbot.crypto_util import get_names_from_csr + return get_names_from_csr(*args, **kwargs) + + def test_extract_one_san(self): + self.assertEqual(['example.com'], self._call( + test_util.load_vector('csr.pem'))) + + def test_extract_two_sans(self): + self.assertEqual(set(('example.com', 'www.example.com',)), set( + self._call(test_util.load_vector('csr-san.pem')))) + + def test_extract_six_sans(self): + self.assertEqual( + set(self._call(test_util.load_vector('csr-6sans.pem'))), + set(("example.com", "example.org", "example.net", + "example.info", "subdomain.example.com", + "other.subdomain.example.com",))) + + def test_parse_non_csr(self): + self.assertRaises(OpenSSL.crypto.Error, self._call, "hello there") + + def test_parse_no_sans(self): + self.assertEqual(["example.org"], + self._call(test_util.load_vector('csr-nosans.pem'))) + + class CertLoaderTest(unittest.TestCase): """Tests for certbot.crypto_util.pyopenssl_load_certificate""" From 01ebab26bfa0eee521e8767411419007efa50814 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 18 May 2016 14:21:57 -0700 Subject: [PATCH 034/192] update pypi for auto --- letsencrypt-auto-source/letsencrypt-auto | 11 +++++++++-- letsencrypt-auto-source/pieces/fetch.py | 2 +- letsencrypt-auto-source/tests/auto_test.py | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index bbb2cda54..dd7dc06ec 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -452,6 +452,11 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + # Install required OS packages: Bootstrap() { @@ -484,8 +489,10 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + 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 + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo @@ -998,7 +1005,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index 38f4aa255..4a2287fff 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -68,7 +68,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 3b7e8731b..2c733f858 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -183,7 +183,7 @@ def run_le_auto(venv_dir, base_url, **kwargs): d = dict(XDG_DATA_HOME=venv_dir, # URL to PyPI-style JSON that tell us the latest released version # of LE: - LE_AUTO_JSON_URL=base_url + 'letsencrypt/json', + LE_AUTO_JSON_URL=base_url + 'certbot/json', # URL to dir containing letsencrypt-auto and letsencrypt-auto.sig: LE_AUTO_DIR_TEMPLATE=base_url + '%s/', # The public key corresponding to signing.key: From 70912be5a9a1000e235ff74d9935cfc2aef9b3c9 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 18 May 2016 14:57:31 -0700 Subject: [PATCH 035/192] Associate --update-registration with register help topic --- certbot/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index d42d77412..7f3779e5b 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -88,7 +88,8 @@ More detailed help: the available topics are: all, automation, paths, security, testing, or any of the subcommands or - plugins (certonly, install, nginx, apache, standalone, webroot, etc) + plugins (certonly, install, register, nginx, apache, standalone, webroot, + etc.) """ @@ -615,7 +616,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "affect you, and will be effective 14 days after posting an " "update to the web site.") helpful.add( - None, "--update-registration", action="store_true", + "register", "--update-registration", action="store_true", help="With the register verb, indicates that details associated " "with an existing registration, such as the e-mail address, " "should be updated, rather than registering a new account.") From 55755d818af57809cd23ec6ccff855ae8a30c50a Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 18 May 2016 15:42:55 -0700 Subject: [PATCH 036/192] update secret pypi? --- letsencrypt-auto-source/tests/auto_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 2c733f858..8018bab0f 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -301,8 +301,8 @@ class AutoTests(TestCase): with ephemeral_dir() as venv_dir: # Serve an unrelated hash signed with the good key (easier than # making a bad key, and a mismatch is a mismatch): - resources = {'': 'letsencrypt/', - 'letsencrypt/json': dumps({'releases': {'99.9.9': None}}), + resources = {'': 'certbot/', + 'certbot/json': dumps({'releases': {'99.9.9': None}}), 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), 'v99.9.9/letsencrypt-auto.sig': signed('something else')} with serving(resources) as base_url: From 2ba5ce9217433cb913edf408477c96a710a91e22 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 18 May 2016 15:55:25 -0700 Subject: [PATCH 037/192] Mention register subcommand in main help --- certbot/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/cli.py b/certbot/cli.py index 7f3779e5b..f8be642d8 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -63,6 +63,7 @@ cert. Major SUBCOMMANDS are: install Install a previously obtained cert in a server renew Renew previously obtained certs that are near expiry revoke Revoke a previously obtained certificate + register Perform tasks related to registering with the CA rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation plugins Display information about installed plugins From e8e009cc854246e9b059783f77506328c417191c Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 18 May 2016 17:00:42 -0700 Subject: [PATCH 038/192] Revert "update secret pypi?" This reverts commit 55755d818af57809cd23ec6ccff855ae8a30c50a. --- letsencrypt-auto-source/tests/auto_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 8018bab0f..2c733f858 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -301,8 +301,8 @@ class AutoTests(TestCase): with ephemeral_dir() as venv_dir: # Serve an unrelated hash signed with the good key (easier than # making a bad key, and a mismatch is a mismatch): - resources = {'': 'certbot/', - 'certbot/json': dumps({'releases': {'99.9.9': None}}), + resources = {'': 'letsencrypt/', + 'letsencrypt/json': dumps({'releases': {'99.9.9': None}}), 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), 'v99.9.9/letsencrypt-auto.sig': signed('something else')} with serving(resources) as base_url: From e7374811294be55b45d8de30a36883f836da6590 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 18:20:27 +0000 Subject: [PATCH 039/192] WIP --- certbot/client.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index dd38e47b0..6dd0420eb 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -292,6 +292,7 @@ class Client(object): key.pem, crypto_util.dump_pyopenssl_chain(chain), configuration.RenewerConfiguration(self.config.namespace)) + def save_certificate(self, certr, chain_cert, cert_path, chain_path, fullchain_path): """Saves the certificate received from the ACME server. @@ -318,12 +319,15 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - + """ if cli.set_by_cli('cert_path'): cert_file = le_util.safe_open(cert_path, chmod=0o644) act_cert_path = cert_path else: cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) + """ + cert_file, act_cert_path = _open_pem_file('cert_path', cert_path) + #import ipdb; ipdb.set_trace try: cert_file.write(cert_pem) @@ -331,7 +335,14 @@ class Client(object): cert_file.close() logger.info("Server issued certificate; certificate written to %s", act_cert_path) - + + if cli.set_by_cli('chain_path'): + #import ipdb; ipdb.set_trace() + pass + if cli.set_by_cli('fullchain_path'): + #import ipdb; ipdb.set_trace() + pass + cert_chain_abspath = None fullchain_abspath = None if chain_cert: @@ -569,6 +580,11 @@ def view_config_changes(config, num=None): rev.recovery_routine() rev.view_config_changes(num) +def _open_pem_file(cli_arg_path, pem_path): + if cli.set_by_cli(cli_arg_path): + return le_util.safe_open(pem_path, chmod=0o644), pem_path + else: + return le_util.unique_file(pem_path, 0o644) def _save_chain(chain_pem, chain_path): """Saves chain_pem at a unique path based on chain_path. From 409640fb87a89487e46be0427c072a05074958a0 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 19 May 2016 12:05:42 -0700 Subject: [PATCH 040/192] le to cb for test package --- letsencrypt-auto-source/tests/auto_test.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 2c733f858..357e8302c 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -258,9 +258,9 @@ class AutoTests(TestCase): with ephemeral_dir() as venv_dir: # This serves a PyPI page with a higher version, a GitHub-alike # with a corresponding le-auto script, and a matching signature. - resources = {'letsencrypt/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': NEW_LE_AUTO, - 'v99.9.9/letsencrypt-auto.sig': NEW_LE_AUTO_SIG} + resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), + 'v99.9.9/certbot-auto': NEW_LE_AUTO, + 'v99.9.9/certbot-auto.sig': NEW_LE_AUTO_SIG} with serving(resources) as base_url: run_letsencrypt_auto = partial( run_le_auto, @@ -301,10 +301,10 @@ class AutoTests(TestCase): with ephemeral_dir() as venv_dir: # Serve an unrelated hash signed with the good key (easier than # making a bad key, and a mismatch is a mismatch): - resources = {'': 'letsencrypt/', - 'letsencrypt/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), - 'v99.9.9/letsencrypt-auto.sig': signed('something else')} + resources = {'': 'certbot/', + 'certbot/json': dumps({'releases': {'99.9.9': None}}), + 'v99.9.9/certbot-auto': build_le_auto(version='99.9.9'), + 'v99.9.9/certbot-auto.sig': signed('something else')} with serving(resources) as base_url: copy(LE_AUTO_PATH, venv_dir) try: @@ -320,8 +320,8 @@ class AutoTests(TestCase): def test_pip_failure(self): """Make sure pip stops us if there is a hash mismatch.""" with ephemeral_dir() as venv_dir: - resources = {'': 'letsencrypt/', - 'letsencrypt/json': dumps({'releases': {'99.9.9': None}})} + resources = {'': 'certbot/', + 'certbot/json': dumps({'releases': {'99.9.9': None}})} with serving(resources) as base_url: # Build a le-auto script embedding a bad requirements file: install_le_auto( From fde151848d4b1a29ada511db77308979ba247989 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 19:11:25 +0000 Subject: [PATCH 041/192] Use set_by_cli for fullchain_path and chain_path --- certbot/client.py | 40 ++++++++++++++++------------------------ certbot/le_util.py | 3 ++- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 6dd0420eb..3475312f0 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -319,15 +319,8 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - """ - if cli.set_by_cli('cert_path'): - cert_file = le_util.safe_open(cert_path, chmod=0o644) - act_cert_path = cert_path - else: - cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) - """ + cert_file, act_cert_path = _open_pem_file('cert_path', cert_path) - #import ipdb; ipdb.set_trace try: cert_file.write(cert_pem) @@ -335,21 +328,20 @@ class Client(object): cert_file.close() logger.info("Server issued certificate; certificate written to %s", act_cert_path) - - if cli.set_by_cli('chain_path'): - #import ipdb; ipdb.set_trace() - pass - if cli.set_by_cli('fullchain_path'): - #import ipdb; ipdb.set_trace() - pass - + cert_chain_abspath = None fullchain_abspath = None if chain_cert: chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) - cert_chain_abspath = _save_chain(chain_pem, chain_path) + + chain_file, act_chain_path =\ + _open_pem_file('chain_path', chain_path) + fullchain_file, act_fullchain_path =\ + _open_pem_file('fullchain_path', fullchain_path) + + cert_chain_abspath = _save_chain(chain_pem, chain_file) fullchain_abspath = _save_chain(cert_pem + chain_pem, - fullchain_path) + fullchain_file) return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath @@ -582,27 +574,27 @@ def view_config_changes(config, num=None): def _open_pem_file(cli_arg_path, pem_path): if cli.set_by_cli(cli_arg_path): - return le_util.safe_open(pem_path, chmod=0o644), pem_path + return le_util.safe_open(pem_path, chmod=0o644),\ + os.path.abspath(pem_path) else: return le_util.unique_file(pem_path, 0o644) -def _save_chain(chain_pem, chain_path): +def _save_chain(chain_pem, chain_file): """Saves chain_pem at a unique path based on chain_path. :param str chain_pem: certificate chain in PEM format - :param str chain_path: candidate path for the cert chain + :param str chain_file: chain file object :returns: absolute path to saved cert chain :rtype: str """ - chain_file, act_chain_path = le_util.unique_file(chain_path, 0o644) try: chain_file.write(chain_pem) finally: chain_file.close() - logger.info("Cert chain written to %s", act_chain_path) + logger.info("Cert chain written to %s", chain_file.name) # This expects a valid chain file - return os.path.abspath(act_chain_path) + return os.path.abspath(chain_file.name) diff --git a/certbot/le_util.py b/certbot/le_util.py index f5148b949..fe2577a4c 100644 --- a/certbot/le_util.py +++ b/certbot/le_util.py @@ -151,7 +151,8 @@ def _unique_file(path, filename_pat, count, mode): while True: current_path = os.path.join(path, filename_pat(count)) try: - return safe_open(current_path, chmod=mode), current_path + return safe_open(current_path, chmod=mode),\ + os.path.abspath(current_path) except OSError as err: # "File exists," is okay, try a different name. if err.errno != errno.EEXIST: From 3aed4fc59d355df158b08f11f96df084b6a53e9b Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Thu, 19 May 2016 14:19:13 -0500 Subject: [PATCH 042/192] Typo: too many self's The extra self will push along the arguments, resulting in the accurate but not very helpful error message: "AttributeError: 'JWKRSA' object has no attribute 'kty'" --- acme/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 280bc8308..c436cc631 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -500,7 +500,7 @@ class DNS(_TokenChallenge): """ return DNSResponse(validation=self.gen_validation( - self, account_key, **kwargs)) + account_key, **kwargs)) def validation_domain_name(self, name): """Domain name for TXT validation record. From 0bb8b0bcd5231c896ee9449bac22d30204381dac Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 19 May 2016 12:27:17 -0700 Subject: [PATCH 043/192] change invocation --- letsencrypt-auto-source/tests/auto_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 357e8302c..6fccdb56e 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -199,7 +199,7 @@ iQIDAQAB **kwargs) env.update(d) return out_and_err( - join(venv_dir, 'letsencrypt-auto') + ' --version', + join(venv_dir, 'certbot-auto') + ' --version', shell=True, env=env) From e1eb3eff164610b7359582478d02c3fbf4b8c6b9 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 19:27:18 +0000 Subject: [PATCH 044/192] Improve code reuse --- certbot/client.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 3475312f0..ee1ab8bb8 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -320,30 +320,27 @@ class Client(object): cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) - cert_file, act_cert_path = _open_pem_file('cert_path', cert_path) + cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path) try: cert_file.write(cert_pem) finally: cert_file.close() logger.info("Server issued certificate; certificate written to %s", - act_cert_path) + abs_cert_path) - cert_chain_abspath = None - fullchain_abspath = None if chain_cert: chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) - chain_file, act_chain_path =\ + chain_file, abs_chain_path =\ _open_pem_file('chain_path', chain_path) - fullchain_file, act_fullchain_path =\ + fullchain_file, abs_fullchain_path =\ _open_pem_file('fullchain_path', fullchain_path) - cert_chain_abspath = _save_chain(chain_pem, chain_file) - fullchain_abspath = _save_chain(cert_pem + chain_pem, - fullchain_file) + _save_chain(chain_pem, chain_file) + _save_chain(cert_pem + chain_pem, fullchain_file - return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath + return abs_cert_path, abs_chain_path, abs_fullchain_path def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path): @@ -577,7 +574,8 @@ def _open_pem_file(cli_arg_path, pem_path): return le_util.safe_open(pem_path, chmod=0o644),\ os.path.abspath(pem_path) else: - return le_util.unique_file(pem_path, 0o644) + uniq = le_util.unique_file(pem_path, 0o644) + return uniq[0], os.path.abspath(uniq) def _save_chain(chain_pem, chain_file): """Saves chain_pem at a unique path based on chain_path. @@ -585,9 +583,6 @@ def _save_chain(chain_pem, chain_file): :param str chain_pem: certificate chain in PEM format :param str chain_file: chain file object - :returns: absolute path to saved cert chain - :rtype: str - """ try: chain_file.write(chain_pem) @@ -595,6 +590,3 @@ def _save_chain(chain_pem, chain_file): chain_file.close() logger.info("Cert chain written to %s", chain_file.name) - - # This expects a valid chain file - return os.path.abspath(chain_file.name) From 501c19ef2a254e44b768eb4cd57e5832f5afb792 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 19:33:04 +0000 Subject: [PATCH 045/192] Syntax --- certbot/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index ee1ab8bb8..8964686e6 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -338,7 +338,7 @@ class Client(object): _open_pem_file('fullchain_path', fullchain_path) _save_chain(chain_pem, chain_file) - _save_chain(cert_pem + chain_pem, fullchain_file + _save_chain(cert_pem + chain_pem, fullchain_file) return abs_cert_path, abs_chain_path, abs_fullchain_path From 3589b25dc3a7d4c0fb2477b21d6039a327bd1b52 Mon Sep 17 00:00:00 2001 From: sagi Date: Thu, 19 May 2016 19:35:38 +0000 Subject: [PATCH 046/192] Make lint happy --- certbot/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 8964686e6..4fc662948 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -292,7 +292,6 @@ class Client(object): key.pem, crypto_util.dump_pyopenssl_chain(chain), configuration.RenewerConfiguration(self.config.namespace)) - def save_certificate(self, certr, chain_cert, cert_path, chain_path, fullchain_path): """Saves the certificate received from the ACME server. From 22badb2380bc405032d47e9f90212a0f1a507fad Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 19 May 2016 17:29:39 -0700 Subject: [PATCH 047/192] tests pass? --- letsencrypt-auto-source/tests/auto_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 6fccdb56e..7e131f4cf 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -199,7 +199,7 @@ iQIDAQAB **kwargs) env.update(d) return out_and_err( - join(venv_dir, 'certbot-auto') + ' --version', + join(venv_dir, 'letsencrypt-auto') + ' --version', shell=True, env=env) @@ -259,8 +259,8 @@ class AutoTests(TestCase): # This serves a PyPI page with a higher version, a GitHub-alike # with a corresponding le-auto script, and a matching signature. resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/certbot-auto': NEW_LE_AUTO, - 'v99.9.9/certbot-auto.sig': NEW_LE_AUTO_SIG} + 'v99.9.9/letsencrypt-auto': NEW_LE_AUTO, + 'v99.9.9/letsencrypt-auto.sig': NEW_LE_AUTO_SIG} with serving(resources) as base_url: run_letsencrypt_auto = partial( run_le_auto, @@ -303,8 +303,8 @@ class AutoTests(TestCase): # making a bad key, and a mismatch is a mismatch): resources = {'': 'certbot/', 'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/certbot-auto': build_le_auto(version='99.9.9'), - 'v99.9.9/certbot-auto.sig': signed('something else')} + 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), + 'v99.9.9/letsencrypt-auto.sig': signed('something else')} with serving(resources) as base_url: copy(LE_AUTO_PATH, venv_dir) try: From 7689de2ad8da074015df2150b455115f3b5816b5 Mon Sep 17 00:00:00 2001 From: sagi Date: Fri, 20 May 2016 01:18:50 +0000 Subject: [PATCH 048/192] Fix tests --- certbot/client.py | 12 +++++++++++- certbot/tests/client_test.py | 8 +++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 4fc662948..818701f08 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -569,12 +569,22 @@ def view_config_changes(config, num=None): rev.view_config_changes(num) def _open_pem_file(cli_arg_path, pem_path): + """Open a pem file. + + If cli_arg_path was set by the client, open that. + Otherwise, uniquify the file path. + + :param str cli_arg_path: the cli arg name, e.g. cert_path + :param str pem_path: the pem file path to open + + :returns a file object + """ if cli.set_by_cli(cli_arg_path): return le_util.safe_open(pem_path, chmod=0o644),\ os.path.abspath(pem_path) else: uniq = le_util.unique_file(pem_path, 0o644) - return uniq[0], os.path.abspath(uniq) + return uniq[0], os.path.abspath(uniq[1]) def _save_chain(chain_pem, chain_file): """Saves chain_pem at a unique path based on chain_path. diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 8ceefe8ae..49596fdee 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -221,7 +221,9 @@ class ClientTest(unittest.TestCase): mock.sentinel.key, domains, self.config.csr_dir) self._check_obtain_certificate() - def test_save_certificate(self): + @mock.patch("certbot.cli.helpful_parser") + def test_save_certificate(self, mock_parser): + # pylint: disable=too-many-locals certs = ["matching_cert.pem", "cert.pem", "cert-san.pem"] tmp_path = tempfile.mkdtemp() os.chmod(tmp_path, 0o755) # TODO: really?? @@ -232,6 +234,10 @@ class ClientTest(unittest.TestCase): candidate_cert_path = os.path.join(tmp_path, "certs", "cert.pem") candidate_chain_path = os.path.join(tmp_path, "chains", "chain.pem") candidate_fullchain_path = os.path.join(tmp_path, "chains", "fullchain.pem") + mock_parser.verb = "certonly" + mock_parser.args = ["--cert-path", candidate_cert_path, + "--chain-path", candidate_chain_path, + "--fullchain-path", candidate_fullchain_path] cert_path, chain_path, fullchain_path = self.client.save_certificate( certr, chain_cert, candidate_cert_path, candidate_chain_path, From 46be2df1999e65e49e6194484f7bee5fea894fcb Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 20 May 2016 13:25:34 -0700 Subject: [PATCH 049/192] fix syntax error --- certbot/reverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/reverter.py b/certbot/reverter.py index fe6d9f24f..b023d18a7 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -489,7 +489,7 @@ class Reverter(object): if not os.path.exists(changes_since_path): logger.info("Rollback checkpoint is empty (no changes made?)") - with open(self.config.changes_since_path) as f: + with open(changes_since_path, 'w') as f: f.write("No changes\n") # Add title to self.config.in_progress_dir CHANGES_SINCE From 32d32dbc1279603a8cdc0cef7e0a0c42563f71ba Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 15:20:28 -0700 Subject: [PATCH 050/192] cli.py PEP8 fixes --- certbot/cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index af4cd3013..3bd565496 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -347,7 +347,6 @@ class HelpfulArgumentParser(object): return parsed_args - def set_test_server(self, parsed_args): """We have --staging/--dry-run; perform sanity check and set config.server""" @@ -370,7 +369,6 @@ class HelpfulArgumentParser(object): parsed_args.tos = True parsed_args.register_unsafely_without_email = True - def handle_csr(self, parsed_args): """Process a --csr flag.""" if parsed_args.verb != "certonly": From 73c4e8f7a40b34e8dfed12d0e784e1aef0d25523 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 16:24:49 -0700 Subject: [PATCH 051/192] Cleanup test_obtain_certificate_from_csr --- certbot/tests/client_test.py | 74 +++++++++++++----------------------- 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 148857be7..b7195a777 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -134,59 +134,39 @@ class ClientTest(unittest.TestCase): self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) - # FIXME move parts of this to crypto_util tests... @mock.patch("certbot.client.logger") def test_obtain_certificate_from_csr(self, mock_logger): self._mock_obtain_certificate() - from certbot import cli test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) - mock_parsed_args = mock.MagicMock() - # The CLI should believe that this is a certonly request, because - # a CSR would not be allowed with other kinds of requests! - mock_parsed_args.verb = "certonly" - with mock.patch("certbot.cli.crypto_util.le_util.CSR") as mock_CSR: - mock_CSR.return_value = test_csr - mock_parsed_args.domains = self.eg_domains[:] - mock_parsed_args.allow_subset_of_names = False - mock_parsed_args.csr = (mock.MagicMock(), mock.MagicMock()) - mock_parser = mock.MagicMock(cli.HelpfulArgumentParser) - cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args) + auth_handler = self.client.auth_handler - # Now provoke an inconsistent domains error... - mock_parsed_args.domains.append("hippopotamus.io") - self.assertRaises(errors.ConfigurationError, - cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args) - - authzr = self.client.auth_handler.get_authorizations(self.eg_domains, False) - - self.assertEqual( - (mock.sentinel.certr, mock.sentinel.chain), - self.client.obtain_certificate_from_csr( - self.eg_domains, - test_csr, - authzr=authzr)) - # and that the cert was obtained correctly - self._check_obtain_certificate() - - # Test for authzr=None - self.assertEqual( - (mock.sentinel.certr, mock.sentinel.chain), - self.client.obtain_certificate_from_csr( - self.eg_domains, - test_csr, - authzr=None)) - - self.client.auth_handler.get_authorizations.assert_called_with( - self.eg_domains) - - # Test for no auth_handler - self.client.auth_handler = None - self.assertRaises( - errors.Error, - self.client.obtain_certificate_from_csr, + authzr = auth_handler.get_authorizations(self.eg_domains, False) + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr( self.eg_domains, - test_csr) - mock_logger.warning.assert_called_once_with(mock.ANY) + test_csr, + authzr=authzr)) + # and that the cert was obtained correctly + self._check_obtain_certificate() + + # Test for authzr=None + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr( + self.eg_domains, + test_csr, + authzr=None)) + auth_handler.get_authorizations.assert_called_with(self.eg_domains) + + # Test for no auth_handler + self.client.auth_handler = None + self.assertRaises( + errors.Error, + self.client.obtain_certificate_from_csr, + self.eg_domains, + test_csr) + mock_logger.warning.assert_called_once_with(mock.ANY) @mock.patch("certbot.client.crypto_util") def test_obtain_certificate(self, mock_crypto_util): From 53286863fefa47b7464340b8b46bbd93b5358932 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 16:26:09 -0700 Subject: [PATCH 052/192] Simplify import_csr_file --- certbot/crypto_util.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index ba368b15b..68e07e059 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -180,33 +180,28 @@ def csr_matches_pubkey(csr, privkey): return False -def import_csr_file(csrfile, contents): +def import_csr_file(csrfile, data): """Import a CSR file, which can be either PEM or DER. :param str csrfile: CSR filename - :param str contents: contens of the CSR file + :param str data: contents of the CSR file - :returns: (le_util.CSR object representing the CSR, - `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, + :returns: (`OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, + le_util.CSR object representing the CSR, list of domains requested in the CSR) - :rtype: tuple + """ - try: - csr = le_util.CSR(file=csrfile, data=contents, form="der") - typ = OpenSSL.crypto.FILETYPE_ASN1 - domains = get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) - except OpenSSL.crypto.Error: + for form, typ in (("der", OpenSSL.crypto.FILETYPE_ASN1,), + ("pem", OpenSSL.crypto.FILETYPE_PEM,),): try: - e1 = traceback.format_exc() - typ = OpenSSL.crypto.FILETYPE_PEM - csr = le_util.CSR(file=csrfile, data=contents, form="pem") - domains = get_sans_from_csr(csr.data, typ) + domains = get_names_from_csr(data, typ) except OpenSSL.crypto.Error: - logger.debug("DER CSR parse error %s", e1) - logger.debug("PEM CSR parse error %s", traceback.format_exc()) - raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) - return typ, csr, domains + logger.debug("CSR parse error (form=%s, typ=%s):", form, typ) + logger.debug(traceback.format_exc()) + continue + return typ, le_util.CSR(file=csrfile, data=data, form=form), domains + raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) def make_key(bits): From 271316a41343593e598f721f3aee3e1621b987e9 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 20 May 2016 16:32:39 -0700 Subject: [PATCH 053/192] cover test --- certbot/reverter.py | 1 + certbot/tests/reverter_test.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/certbot/reverter.py b/certbot/reverter.py index b023d18a7..16ee5d8a4 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -7,6 +7,7 @@ import shutil import time import traceback + import zope.component from certbot import constants diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index eda5ffb36..c79c72a48 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -34,6 +34,20 @@ class ReverterCheckpointLocalTest(unittest.TestCase): logging.disable(logging.NOTSET) + @mock.patch("certbot.reverter.Reverter._read_and_append") + def test_no_change(self, mock_read): + mock_read.side_effect = OSError("cannot even") + try: + self.reverter.add_to_checkpoint(self.sets[0], "save1") + except: + pass + self.reverter.finalize_checkpoint("blah") + path = os.listdir(self.reverter.config.backup_dir)[0] + no_change = os.path.join(self.reverter.config.backup_dir, path, "CHANGES_SINCE") + with open(no_change, "r") as f: + x = f.read() + self.assertTrue("No changes" in x) + def test_basic_add_to_temp_checkpoint(self): # These shouldn't conflict even though they are both named config.txt self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") From a48afd498c9c7a022eff72d8f903879f824cbe14 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 16:52:35 -0700 Subject: [PATCH 054/192] Start import_csr_file tests --- certbot/tests/crypto_util_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index eade4861f..af8308fd8 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -10,6 +10,7 @@ import zope.component from certbot import errors from certbot import interfaces +from certbot import le_util from certbot.tests import test_util @@ -159,6 +160,27 @@ class CSRMatchesPubkeyTest(unittest.TestCase): test_util.load_vector('csr.pem'), RSA256_KEY)) +class ImportCSRFileTest(unittest.TestCase): + """Tests for certbot.certbot_util.import_csr_file.""" + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.crypto_util import import_csr_file + return import_csr_file(*args, **kwargs) + + def test_der_csr(self): + csrfile = test_util.vector_path('csr.der') + data = test_util.load_vector('csr.der') + + self.assertEqual( + (OpenSSL.crypto.FILETYPE_ASN1, + le_util.CSR(file=csrfile, + data=data, + form="der"), + ["example.com"],), + self._call(csrfile, data)) + + class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for certbot.crypto_util.make_key.""" From 953d4957b8e32a74d894c2d3eeab56e0105ebf94 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 16:57:13 -0700 Subject: [PATCH 055/192] Add csr test --- certbot/tests/crypto_util_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index af8308fd8..d5a016320 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -180,6 +180,18 @@ class ImportCSRFileTest(unittest.TestCase): ["example.com"],), self._call(csrfile, data)) + def test_pem_csr(self): + csrfile = test_util.vector_path('csr.pem') + data = test_util.load_vector('csr.pem') + + self.assertEqual( + (OpenSSL.crypto.FILETYPE_PEM, + le_util.CSR(file=csrfile, + data=data, + form="pem"), + ["example.com"],), + self._call(csrfile, data)) + class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for certbot.crypto_util.make_key.""" From 2c4c8c081c80d00be04149afd7da2396f52a0a93 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 17:00:06 -0700 Subject: [PATCH 056/192] Test bad csr --- certbot/tests/crypto_util_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index d5a016320..eeea0f4ab 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -192,6 +192,11 @@ class ImportCSRFileTest(unittest.TestCase): ["example.com"],), self._call(csrfile, data)) + def test_bad_csr(self): + self.assertRaises(errors.Error, self._call, + test_util.vector_path('cert.pem'), + test_util.load_vector('cert.pem')) + class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for certbot.crypto_util.make_key.""" From 3c11733006b03da942d0ee43a8d16c1c6bd40d77 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 17:32:37 -0700 Subject: [PATCH 057/192] fix --csr and --allow-subset-of-names test --- certbot/tests/cli_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index d7965a24e..62ec36d2a 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -349,8 +349,9 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods ['-d', '204.11.231.35']) def test_csr_with_besteffort(self): - args = ["--csr", CSR, "--allow-subset-of-names"] - self.assertRaises(errors.Error, self._call, args) + self.assertRaises( + errors.Error, self._call, + 'certonly --csr {0} --allow-subset-of-names'.format(CSR).split()) def test_run_with_csr(self): # This is an error because you can only use --csr with certonly From 0b85a8f1c8d26accccd148701f2e7ddd6065a6e2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 17:37:48 -0700 Subject: [PATCH 058/192] Add a test for a CSR with no domains --- certbot/tests/cli_test.py | 6 ++++++ certbot/tests/testdata/csr-nonames.pem | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 certbot/tests/testdata/csr-nonames.pem diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 62ec36d2a..c24da4989 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -362,6 +362,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods return assert False, "Expected supplying --csr to fail with default verb" + def test_csr_with_no_domains(self): + self.assertRaises( + errors.Error, self._call, + 'certonly --csr {0}'.format( + test_util.vector_path('csr-nonames.pem')).split()) + def _get_argument_parser(self): plugins = disco.PluginsRegistry.find_all() return functools.partial(cli.prepare_and_parse_args, plugins) diff --git a/certbot/tests/testdata/csr-nonames.pem b/certbot/tests/testdata/csr-nonames.pem new file mode 100644 index 000000000..abe1029ca --- /dev/null +++ b/certbot/tests/testdata/csr-nonames.pem @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIH/MIGqAgEAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEF +AANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+ +6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoAAwDQYJKoZIhvcNAQELBQAD +QQBt9XLSZ9DGfWcGGaBUTCiSY7lWBegpNlCeo8pK3ydWmKpjcza+j7lF5paph2LH +lKWVQ8+xwYMscGWK0NApHGco +-----END CERTIFICATE REQUEST----- From 5cb0e0f264ef94187b774561329d2ab432e2634e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 May 2016 17:41:58 -0700 Subject: [PATCH 059/192] Add test for csr with inconsistent domains --- certbot/tests/cli_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index c24da4989..4ae69e69d 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -368,6 +368,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods 'certonly --csr {0}'.format( test_util.vector_path('csr-nonames.pem')).split()) + def test_csr_with_inconsistent_domains(self): + self.assertRaises( + errors.Error, self._call, + 'certonly -d example.org --csr {0}'.format(CSR).split()) + def _get_argument_parser(self): plugins = disco.PluginsRegistry.find_all() return functools.partial(cli.prepare_and_parse_args, plugins) From 3105fe8a34618f22b906ef8f4875dbf5f1695dca Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 21 May 2016 13:26:59 -0700 Subject: [PATCH 060/192] Disable too-many-statements on parser setup --- certbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index 7898c4c0b..356e71c56 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -567,7 +567,7 @@ class HelpfulArgumentParser(object): return dict([(t, t == chosen_topic) for t in self.help_topics]) -def prepare_and_parse_args(plugins, args, detect_defaults=False): +def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements """Returns parsed command line arguments. :param .PluginsRegistry plugins: available plugins From fab9f8db78a52cd7019867a4bf5748cf8f932097 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 21 May 2016 13:27:12 -0700 Subject: [PATCH 061/192] Complete register functionality (for now) --- certbot/main.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 7b50efd0f..fc54c8615 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -370,11 +370,27 @@ def _init_le_client(config, authenticator, installer): def register(config, unused_plugins): """Create or modify accounts on the server.""" - # Currently, only --update-registration is implemented. Issue #2446 - # calls for a fuller register verb, to allow better separation of - # account management from obtaining certs. + # Portion of _determine_account logic to see whether accounts already + # exist or not. + account_storage = account.AccountFileStorage(config) + accounts = account_storage.find_all() + + # registering a new account if not config.update_registration: - return "Currently, only register --update-registration is implemented." + if len(accounts) > 0: + # TODO: add a flag to register a duplicate account (this will + # also require extending _determine_account's behavior + # or else extracting the registration code from there) + return ("There is an existing account; registration of a " + "duplicate account with this command is currently " + "unsupported.") + # _determine_account will register an account + _determine_account(config) + return + + # --update-registration + if len(accounts) == 0: + return "Could not find an existing account to update." if config.email is None: return ("Currently, --update-registration can only change the e-mail " "address\nassociated with an account. A new e-mail address is " @@ -392,6 +408,7 @@ def register(config, unused_plugins): # We rely on an ACME exception to interrupt this process if it didn't work. print("Registration change succeeded. New registration data:\n") print(query_data) + return def install(config, plugins): From e7c8b73083a33cc942edeb9d69774b18f4895c9b Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sun, 22 May 2016 09:50:27 -0700 Subject: [PATCH 062/192] Test coverage for register verb --- certbot/tests/cli_test.py | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index d7965a24e..1b6d032a8 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -14,6 +14,7 @@ import mock import six from six.moves import reload_module # pylint: disable=import-error +from acme import errors as acme_errors from acme import jose from certbot import account @@ -888,6 +889,72 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call(['-c', test_util.vector_path('cli.ini')]) self.assertTrue(mocked_run.called) + def test_register(self): + with mock.patch('certbot.main.client') as mocked_client: + acc = mock.MagicMock() + acc.id = "imaginary_account" + mocked_client.register.return_value = (acc, "worked") + self._call_no_clientmock(["register", "--email", "user@example.org"]) + # TODO: It would be more correct to explicitly check that + # _determine_account() gets called in the above case, + # but coverage statistics should also show that it did. + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + x = self._call_no_clientmock(["register", "--email", "user@example.org"]) + assert "There is an existing account" in x[0] + + def test_update_registration_no_existing_accounts(self): + # with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = [] + x = self._call_no_clientmock( + ["register", "--update-registration", "--email", + "user@example.org"]) + assert "Could not find an existing account" in x[0] + + def test_update_registration_no_email(self): + # This test will become obsolete when register --update-registration + # supports updating something other than the e-mail address! + # with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + x = self._call_no_clientmock(["register", "--update-registration"]) + assert "can only change the e-mail" in x[0] + + def test_update_registration_with_email(self): + with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + with mock.patch('certbot.main._determine_account') as mocked_det: + with mock.patch('certbot.main.client') as mocked_client: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + mocked_det.return_value = ("a", "b") + acme_client = mock.MagicMock() + mocked_client.Client.return_value = acme_client + # Currently the update_registration() call always + # raises a harmless acme_errors.UnexpectedUpdate. + # If this is fixed, we should get rid of both this + # side effect and the corresponding try/catch in + # main.register(). + uu = acme_errors.UnexpectedUpdate + acme_client.acme.update_registration.side_effect = uu + x = self._call_no_clientmock( + ["register", "--update-registration", "--email", + "user@example.org"]) + # When registration change succeeds, the return value + # of register() is None + assert x[0] is None + # and we got far enough to query the registration from + # the server + assert acme_client.acme.query_registration.call_count == 1 + class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" From d4de026372780f079af3e6b811da991bad824ffe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:37:47 -0700 Subject: [PATCH 063/192] Add get_strict_version --- certbot/le_util.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/certbot/le_util.py b/certbot/le_util.py index f5148b949..1e5997d92 100644 --- a/certbot/le_util.py +++ b/certbot/le_util.py @@ -1,6 +1,9 @@ """Utilities for all Certbot.""" import argparse import collections +# distutils.version under virtualenv confuses pylint +# For more info, see: https://github.com/PyCQA/pylint/issues/73 +import distutils.version # pylint: disable=import-error,no-name-in-module import errno import logging import os @@ -342,3 +345,17 @@ def enforce_domain_sanity(domain): if not fqdn.match(domain): raise errors.ConfigurationError("Requested domain {0} is not a FQDN".format(domain)) return domain + + +def get_strict_version(normalized): + """Converts a normalized version to a strict version. + + :param str normalized: normalized version string + + :returns: An equivalent strict version + :rtype: distutils.version.StrictVersion + + """ + # strict version ending with "a" and a number designates a pre-release + # pylint: disable=no-member + return distutils.version.StrictVersion(normalized.replace(".dev", "a")) From 40a0354b360214485fbd19d90e24c9c9d040856c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:50:14 -0700 Subject: [PATCH 064/192] Start get_strict_version tests --- certbot/tests/le_util_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index b6da4525f..db76615ee 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -339,5 +339,18 @@ class EnforceDomainSanityTest(unittest.TestCase): u"eichh\u00f6rnchen.example.com") +class GetStrictVersionTest(unittest.TestCase): + """Tests for certbot.le_util.get_strict_version.""" + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.le_util import get_strict_version + return get_strict_version(*args, **kwargs) + + def test_two_dev_versions(self): + self.assertTrue( + self._call("0.0.0.dev20151006") < self._call("0.0.0.dev20151008")) + + if __name__ == "__main__": unittest.main() # pragma: no cover From 4caba1fc75749eb6c4f45dd61e314f62e26b962b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:53:09 -0700 Subject: [PATCH 065/192] Test mixed versions --- certbot/tests/le_util_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index db76615ee..2dcecae2c 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -351,6 +351,10 @@ class GetStrictVersionTest(unittest.TestCase): self.assertTrue( self._call("0.0.0.dev20151006") < self._call("0.0.0.dev20151008")) + def test_one_dev_one_release_version(self): + self.assertTrue(self._call("1.0.0.dev0") < self._call("1.0.0")) + self.assertTrue(self._call("1.0.0") < self._call("1.0.1.dev0")) + if __name__ == "__main__": unittest.main() # pragma: no cover From ac37b9de6fdd5f935df4a5f83e0840f88df24d29 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:55:17 -0700 Subject: [PATCH 066/192] test release version comparison --- certbot/tests/le_util_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index 2dcecae2c..5bf93a406 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -355,6 +355,11 @@ class GetStrictVersionTest(unittest.TestCase): self.assertTrue(self._call("1.0.0.dev0") < self._call("1.0.0")) self.assertTrue(self._call("1.0.0") < self._call("1.0.1.dev0")) + def test_two_release_versions(self): + self.assertTrue(self._call("0.0.0") < self._call("0.0.1")) + self.assertTrue(self._call("0.0.0") < self._call("0.1.0")) + self.assertTrue(self._call("0.0.0") < self._call("1.0.0")) + if __name__ == "__main__": unittest.main() # pragma: no cover From 536234c5595347a472060beccdd9fd2e83791483 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 13:59:16 -0700 Subject: [PATCH 067/192] try and catch problems if we do something silly with our version in the future --- certbot/tests/le_util_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/tests/le_util_test.py b/certbot/tests/le_util_test.py index 5bf93a406..6e4eef0f1 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/le_util_test.py @@ -10,6 +10,7 @@ import unittest import mock import six +import certbot from certbot import errors @@ -360,6 +361,11 @@ class GetStrictVersionTest(unittest.TestCase): self.assertTrue(self._call("0.0.0") < self._call("0.1.0")) self.assertTrue(self._call("0.0.0") < self._call("1.0.0")) + def test_current_version(self): + current_version = self._call(certbot.__version__) + self.assertTrue(self._call("0.6.0") < current_version) + self.assertTrue(current_version < self._call("99.99.99")) + if __name__ == "__main__": unittest.main() # pragma: no cover From 0e9aec20a76d06895173ef815962447d75bb87d8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 14:04:13 -0700 Subject: [PATCH 068/192] Add CURRENT_VERSION constant --- certbot/storage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot/storage.py b/certbot/storage.py index c4bfb3e28..e2b26d322 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -8,6 +8,7 @@ import configobj import parsedatetime import pytz +import certbot from certbot import constants from certbot import crypto_util from certbot import errors @@ -17,6 +18,7 @@ from certbot import le_util logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") +CURRENT_VERSION = le_util.get_strict_version(certbot.__version__) def config_with_defaults(config=None): From c3c9441a599226efe261d140d547977eb6ac8d71 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 14:09:31 -0700 Subject: [PATCH 069/192] Save version in renewal config file --- certbot/storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/storage.py b/certbot/storage.py index e2b26d322..d4a469ca1 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -65,6 +65,7 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data): """ config = configobj.ConfigObj(o_filename) + config["version"] = certbot.__version__ for kind in ALL_FOUR: config[kind] = target[kind] From 3576d372a67882a584c207b7bf720c10fc8d69c6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 14:32:43 -0700 Subject: [PATCH 070/192] Add warning about parsing old configuration file --- certbot/storage.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/certbot/storage.py b/certbot/storage.py index d4a469ca1..6c13eb844 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -262,6 +262,14 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes "renewal config file {0} is missing a required " "file reference".format(self.configfile)) + conf_version = self.configuration.get("version") + if (conf_version is not None and + le_util.get_strict_version(conf_version) > CURRENT_VERSION): + logger.warning( + "Attempting to parse the version %s renewal configuration " + "file found at %s with version %s of Certbot. This might not " + "work.", conf_version, config_filename, certbot.__version__) + self.cert = self.configuration["cert"] self.privkey = self.configuration["privkey"] self.chain = self.configuration["chain"] From ea53a14b57910bf9749d9304a3e93e02bea36d02 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 14:58:14 -0700 Subject: [PATCH 071/192] test version is stored --- certbot/tests/storage_test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index be626edc5..aeba5c3ae 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -10,6 +10,7 @@ import configobj import mock import pytz +import certbot from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR @@ -760,11 +761,14 @@ class RenewableCertTests(BaseRenewableCertTest): with open(temp2, "r") as f: content = f.read() # useful value was updated - assert "useful = new_value" in content + self.assertTrue("useful = new_value" in content) # associated comment was preserved - assert "A useful value" in content + self.assertTrue("A useful value" in content) # useless value was deleted - assert "useless" not in content + self.assertTrue("useless" not in content) + # check version was stored + self.assertTrue("version = {0}".format(certbot.__version__) in content) + if __name__ == "__main__": unittest.main() # pragma: no cover From 15ddf2ea32a4f020b655c7ac4ca6d62cb0cafd50 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 23 May 2016 15:16:43 -0700 Subject: [PATCH 072/192] Test init with newer conf file --- certbot/tests/storage_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index aeba5c3ae..b1444f311 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -138,6 +138,18 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertRaises(errors.CertStorageError, storage.RenewableCert, config.filename, self.cli_config) + def test_renewal_newer_version(self): + from certbot import storage + + self._write_out_ex_kinds() + self.config["version"] = "99.99.99" + self.config.write() + + with mock.patch("certbot.storage.logger") as mock_logger: + storage.RenewableCert(self.config.filename, self.cli_config) + self.assertTrue(mock_logger.warning.called) + self.assertTrue("version" in mock_logger.warning.call_args[0][0]) + def test_consistent(self): # pylint: disable=too-many-statements,protected-access oldcert = self.test_rc.cert From b54497d8145e0999ac3f163ae38e3bc70a7a5549 Mon Sep 17 00:00:00 2001 From: sagi Date: Tue, 24 May 2016 19:33:13 +0000 Subject: [PATCH 073/192] Fix chain filename --- 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 a1245e1c9..323ea004b 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -44,7 +44,7 @@ common auth --csr "$CSR_PATH" \ --cert-path "${root}/csr/cert.pem" \ --chain-path "${root}/csr/chain.pem" openssl x509 -in "${root}/csr/cert.pem" -text -openssl x509 -in "${root}/csr/0000_chain.pem" -text +openssl x509 -in "${root}/csr/chain.pem" -text common --domains le3.wtf install \ --cert-path "${root}/csr/cert.pem" \ From b1eff0fe3528f0cef2a077b6dcee777d1dbebb8c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 13:03:53 -0700 Subject: [PATCH 074/192] Build le-auto to bring it up to date --- letsencrypt-auto-source/letsencrypt-auto | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index eb5561070..ea085454c 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -425,7 +425,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." From 70bb7ff68f2a9eb5fac7b6cc494a50dce99ade20 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 13:08:10 -0700 Subject: [PATCH 075/192] fixes #3060 --- letsencrypt-auto-source/letsencrypt-auto | 3 +-- letsencrypt-auto-source/letsencrypt-auto.template | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index ea085454c..2de4b053e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -935,6 +935,7 @@ else if [ "$NO_SELF_UPGRADE" != 1 ]; then TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: @@ -1089,8 +1090,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index f1ed82c4c..116894a93 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -291,6 +291,7 @@ else if [ "$NO_SELF_UPGRADE" != 1 ]; then TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" {{ fetch.py }} @@ -319,8 +320,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. From c606273d1489a27c50376fca6244968a4ccde06a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 13:16:21 -0700 Subject: [PATCH 076/192] use TEMP_DIR trap consistently --- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-auto-source/letsencrypt-auto.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 2de4b053e..b65c29a44 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -532,6 +532,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -889,7 +890,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 116894a93..43d8bc7e1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -229,6 +229,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -245,7 +246,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" From 420e64f03951bbce7737d0b0ce05fad9070763db Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 13:43:45 -0700 Subject: [PATCH 077/192] Simplify webroot chown and rm errors --- certbot/plugins/webroot.py | 22 +++++---------------- certbot/plugins/webroot_test.py | 35 ++------------------------------- 2 files changed, 7 insertions(+), 50 deletions(-) diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index c344954ae..508000d77 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -181,12 +181,8 @@ to serve all files under specified web root ({0}).""" os.chown(self.full_roots[name], stat_path.st_uid, stat_path.st_gid) except OSError as exception: - if exception.errno == errno.EACCES: - logger.debug("Insufficient permissions to change owner and uid - ignoring") - else: - raise errors.PluginError( - "Couldn't create root for {0} http-01 " - "challenge responses: {1}", name, exception) + logger.debug("Unable to change owner and uid of webroot directory") + logger.debug("Error was: %s", exception) except OSError as exception: if exception.errno != errno.EEXIST: @@ -235,17 +231,9 @@ to serve all files under specified web root ({0}).""" logger.debug("All challenges cleaned up, removing %s", root_path) except OSError as exc: - if exc.errno == errno.ENOTEMPTY: - logger.debug("Challenges cleaned up but %s not empty", - root_path) - elif exc.errno == errno.EACCES: - logger.debug("Challenges cleaned up but no permissions for %s", - root_path) - elif exc.errno == errno.ENOENT: - logger.debug("Challenges cleaned up, %s does not exists", - root_path) - else: - raise + logger.debug( + "Unable to clean up challenge directory %s", root_path) + logger.debug("Error was: %s", exc) class _WebrootMapAction(argparse.Action): diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py index ab2a9e9a4..5d784a75c 100644 --- a/certbot/plugins/webroot_test.py +++ b/certbot/plugins/webroot_test.py @@ -138,15 +138,10 @@ class AuthenticatorTest(unittest.TestCase): os.chmod(self.path, 0o700) @mock.patch("certbot.plugins.webroot.os.chown") - def test_failed_chown_eacces(self, mock_chown): + def test_failed_chown(self, mock_chown): mock_chown.side_effect = OSError(errno.EACCES, "msg") self.auth.perform([self.achall]) # exception caught and logged - @mock.patch("certbot.plugins.webroot.os.chown") - def test_failed_chown_not_eacces(self, mock_chown): - mock_chown.side_effect = OSError() - self.assertRaises(errors.PluginError, self.auth.perform, []) - def test_perform_permissions(self): self.auth.prepare() @@ -200,7 +195,7 @@ class AuthenticatorTest(unittest.TestCase): os.rmdir(leftover_path) @mock.patch('os.rmdir') - def test_cleanup_permission_denied(self, mock_rmdir): + def test_cleanup_failure(self, mock_rmdir): self.auth.prepare() self.auth.perform([self.achall]) @@ -212,32 +207,6 @@ class AuthenticatorTest(unittest.TestCase): self.assertFalse(os.path.exists(self.validation_path)) self.assertTrue(os.path.exists(self.root_challenge_path)) - @mock.patch('os.rmdir') - def test_cleanup_oserror(self, mock_rmdir): - self.auth.prepare() - self.auth.perform([self.achall]) - - os_error = OSError() - os_error.errno = errno.EPERM - mock_rmdir.side_effect = os_error - - self.assertRaises(OSError, self.auth.cleanup, [self.achall]) - self.assertFalse(os.path.exists(self.validation_path)) - self.assertTrue(os.path.exists(self.root_challenge_path)) - - @mock.patch('os.rmdir') - def test_cleanup_file_not_exists(self, mock_rmdir): - self.auth.prepare() - self.auth.perform([self.achall]) - - os_error = OSError() - os_error.errno = errno.ENOENT - mock_rmdir.side_effect = os_error - - self.auth.cleanup([self.achall]) - self.assertFalse(os.path.exists(self.validation_path)) - self.assertTrue(os.path.exists(self.root_challenge_path)) - class WebrootActionTest(unittest.TestCase): """Tests for webroot argparse actions.""" From c01e2c259af0e200222e66bdf27434f3dba47613 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 24 May 2016 15:38:03 -0700 Subject: [PATCH 078/192] Check out Boulder master instead of branch. --- tests/boulder-fetch.sh | 2 +- tests/letstest/scripts/boulder_install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index 11e36835a..a09d0adf9 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -3,7 +3,7 @@ set -xe # Check out special branch until latest docker changes land in Boulder master. -git clone -b rev-rev https://github.com/letsencrypt/boulder $BOULDERPATH +git clone https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d diff --git a/tests/letstest/scripts/boulder_install.sh b/tests/letstest/scripts/boulder_install.sh index 936a64802..426642880 100755 --- a/tests/letstest/scripts/boulder_install.sh +++ b/tests/letstest/scripts/boulder_install.sh @@ -3,7 +3,7 @@ # >>>> only tested on Ubuntu 14.04LTS <<<< # Check out special branch until latest docker changes land in Boulder master. -git clone -b rev-rev https://github.com/letsencrypt/boulder $BOULDERPATH +git clone https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d From 57e6d1995bebf2a700f83e1dfaff2c626d6d8d18 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 16:32:03 -0700 Subject: [PATCH 079/192] log louder --- certbot/plugins/webroot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index 508000d77..624ee2ff4 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -181,7 +181,7 @@ to serve all files under specified web root ({0}).""" os.chown(self.full_roots[name], stat_path.st_uid, stat_path.st_gid) except OSError as exception: - logger.debug("Unable to change owner and uid of webroot directory") + logger.info("Unable to change owner and uid of webroot directory") logger.debug("Error was: %s", exception) except OSError as exception: @@ -231,7 +231,7 @@ to serve all files under specified web root ({0}).""" logger.debug("All challenges cleaned up, removing %s", root_path) except OSError as exc: - logger.debug( + logger.info( "Unable to clean up challenge directory %s", root_path) logger.debug("Error was: %s", exc) From 87d0e938ad8912cd1b17057a3009f2044e46f5c7 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Thu, 5 May 2016 09:09:41 +0200 Subject: [PATCH 080/192] Adding --dialog argument --- certbot/cli.py | 3 +++ certbot/main.py | 3 ++- certbot/tests/cli_test.py | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index e2c57595b..f96bf2656 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -569,6 +569,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " "which ones are required if it finds one missing") + helpful.add( + None, "--dialog", dest="dialog_mode", action="store_true", + help="Run using dialog") helpful.add( None, "--dry-run", action="store_true", dest="dry_run", help="Perform a test run of the client, obtaining test (invalid) certs" diff --git a/certbot/main.py b/certbot/main.py index 309889e8e..161e7d7cd 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -677,8 +677,9 @@ def main(cli_args=sys.argv[1:]): displayer = display_util.NoninteractiveDisplay(sys.stdout) elif config.text_mode: displayer = display_util.FileDisplay(sys.stdout) + elif config.dialog_mode: + displayer = display_util.NcursesDisplay() elif config.verb == "renew": - config.noninteractive_mode = True displayer = display_util.NoninteractiveDisplay(sys.stdout) else: displayer = display_util.NcursesDisplay() diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 31056cafe..ca33f4524 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -149,6 +149,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args.extend(['--email', 'io@io.is']) self._cli_missing_flag(args, "--agree-tos") + @mock.patch('certbot.main.renew') + def test_gui(self, renew): + args = ['renew', '--dialog'] + self._call(args) + self.assertFalse(renew.call_args[0][0].noninteractive_mode) + @mock.patch('certbot.main.client.acme_client.Client') @mock.patch('certbot.main._determine_account') @mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate') From 8772a9846f5c7e834dc23863e4905f5676159fc0 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 02:52:47 +0200 Subject: [PATCH 081/192] Raising error on conflicting args --- certbot/main.py | 10 ++++++++++ certbot/tests/cli_test.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 161e7d7cd..d900c65fa 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -669,6 +669,16 @@ def main(cli_args=sys.argv[1:]): sys.excepthook = functools.partial(_handle_exception, config=config) + # Avoid conflicting args + conficting_args = ["quiet", "noninteractive_mode", "text_mode"] + if config.dialog_mode: + for arg in conficting_args: + if getattr(config, arg): + raise errors.Error( + ("Conflicting values for displayer." + " {0} conflicts with dialog_mode").format(arg) + ) + # Displayer if config.quiet: config.noninteractive_mode = True diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index ca33f4524..64cf98c2c 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -49,7 +49,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.logs_dir = os.path.join(self.tmp_dir, 'logs') self.standard_args = ['--config-dir', self.config_dir, '--work-dir', self.work_dir, - '--logs-dir', self.logs_dir, '--text'] + '--logs-dir', self.logs_dir] def tearDown(self): shutil.rmtree(self.tmp_dir) From 1fd44d9302b5fa374dfc0c87416eb6006bccc8d2 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 03:55:52 +0200 Subject: [PATCH 082/192] Fixing tests --- certbot/cli.py | 3 +++ certbot/tests/cli_test.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index f96bf2656..e2878c1e9 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -552,6 +552,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): :rtype: argparse.Namespace """ + + # pylint: disable=too-many-statements + helpful = HelpfulArgumentParser(args, plugins, detect_defaults) # --help is automatically provided by argparse diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 64cf98c2c..7e352febb 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -49,7 +49,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.logs_dir = os.path.join(self.tmp_dir, 'logs') self.standard_args = ['--config-dir', self.config_dir, '--work-dir', self.work_dir, - '--logs-dir', self.logs_dir] + '--logs-dir', self.logs_dir, '--text'] def tearDown(self): shutil.rmtree(self.tmp_dir) @@ -152,6 +152,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods @mock.patch('certbot.main.renew') def test_gui(self, renew): args = ['renew', '--dialog'] + # --text conflicts with --dialog + self.standard_args.remove('--text') self._call(args) self.assertFalse(renew.call_args[0][0].noninteractive_mode) From 353abaa608c74f2a331ee920515aa713d6dd1827 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 04:59:09 +0200 Subject: [PATCH 083/192] Adding args conflict test to main_test.py --- certbot/tests/main_test.py | 39 +++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 66cba64a3..e3f8b860d 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1,15 +1,49 @@ """Tests for certbot.main.""" +import os +import shutil +import tempfile import unittest - import mock - +import six from certbot import cli from certbot import configuration +from certbot import errors +from certbot import main from certbot.plugins import disco as plugins_disco +class MainTest(unittest.TestCase): + """Tests for certbot.main""" + + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + self.config_dir = os.path.join(self.tmp_dir, 'config') + self.work_dir = os.path.join(self.tmp_dir, 'work') + self.logs_dir = os.path.join(self.tmp_dir, 'logs') + self.standard_args = ['--config-dir', self.config_dir, + '--work-dir', self.work_dir, + '--logs-dir', self.logs_dir, '--text'] + + def tearDown(self): + shutil.rmtree(self.tmp_dir) + + def _call(self, args, stdout=None): + "Run the client with output streams mocked out" + args = self.standard_args + args + + toy_stdout = stdout if stdout else six.StringIO() + with mock.patch('certbot.main.sys.stdout', new=toy_stdout): + with mock.patch('certbot.main.sys.stderr') as stderr: + ret = main.main(args[:]) # NOTE: parser can alter its args! + return ret, toy_stdout, stderr + + def test_args_conflict(self): + args = ['renew', '--dialog', '--text'] + self.assertRaises(errors.Error, self._call, args) + + class ObtainCertTest(unittest.TestCase): """Tests for certbot.main.obtain_cert.""" @@ -26,7 +60,6 @@ class ObtainCertTest(unittest.TestCase): config = configuration.NamespaceConfig( cli.prepare_and_parse_args(plugins, args)) - from certbot import main with mock.patch('certbot.main._init_le_client') as mock_init: main.obtain_cert(config, plugins) From e93aeb88dd4da8e26fd6a4c257234ba990aae403 Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 04:19:18 +0000 Subject: [PATCH 084/192] Fix docs --- certbot/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/client.py b/certbot/client.py index 514da879e..9ce326822 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -580,7 +580,8 @@ def _open_pem_file(cli_arg_path, pem_path): :param str cli_arg_path: the cli arg name, e.g. cert_path :param str pem_path: the pem file path to open - :returns a file object + :returns: a tuple of file object and its absolute file path + """ if cli.set_by_cli(cli_arg_path): return le_util.safe_open(pem_path, chmod=0o644),\ From 13e165e2f9ef3af630d6dbe74082aa689f35be31 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 24 May 2016 21:30:45 -0700 Subject: [PATCH 085/192] add exception type --- certbot/tests/reverter_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index c79c72a48..85234b76a 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -39,7 +39,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): mock_read.side_effect = OSError("cannot even") try: self.reverter.add_to_checkpoint(self.sets[0], "save1") - except: + except OSError: pass self.reverter.finalize_checkpoint("blah") path = os.listdir(self.reverter.config.backup_dir)[0] From 4a85d59d9353e7a0567c0bc56c9729d75b96d209 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 24 May 2016 22:03:30 -0700 Subject: [PATCH 086/192] Add test for missing renewal conf version --- certbot/tests/storage_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index b1444f311..f19b7d89d 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -138,6 +138,16 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertRaises(errors.CertStorageError, storage.RenewableCert, config.filename, self.cli_config) + def test_no_renewal_version(self): + from certbot import storage + + self._write_out_ex_kinds() + self.assertTrue("version" not in self.config) + + with mock.patch("certbot.storage.logger") as mock_logger: + storage.RenewableCert(self.config.filename, self.cli_config) + self.assertFalse(mock_logger.warning.called) + def test_renewal_newer_version(self): from certbot import storage From b158d03e9754e8674903fa7bc3ea00642e5c361c Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 16:30:29 +0200 Subject: [PATCH 087/192] Revert removal of a needed line --- certbot/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/main.py b/certbot/main.py index d900c65fa..0f2aece1e 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -690,6 +690,7 @@ def main(cli_args=sys.argv[1:]): elif config.dialog_mode: displayer = display_util.NcursesDisplay() elif config.verb == "renew": + config.noninteractive_mode = True displayer = display_util.NoninteractiveDisplay(sys.stdout) else: displayer = display_util.NcursesDisplay() From d1df72d63c7eb9e590d05651f0f1a41caf234a1d Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 19:06:25 +0000 Subject: [PATCH 088/192] Add the chain_cert is None case --- certbot/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 9ce326822..ba31f8760 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -328,7 +328,9 @@ class Client(object): logger.info("Server issued certificate; certificate written to %s", abs_cert_path) - if chain_cert: + if not chain_cert: + return abs_cert_path, None, None + else: chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) chain_file, abs_chain_path =\ @@ -339,7 +341,7 @@ class Client(object): _save_chain(chain_pem, chain_file) _save_chain(cert_pem + chain_pem, fullchain_file) - return abs_cert_path, abs_chain_path, abs_fullchain_path + return abs_cert_path, abs_chain_path, abs_fullchain_path def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path): From b3aeeefe20aae784fb6959e935dde87a3c761feb Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 20:03:45 +0000 Subject: [PATCH 089/192] Autoconfigure OCSP Stapling with --must-staple --- certbot/client.py | 5 +++-- certbot/interfaces.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 0159d3946..a81b7cd70 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -398,6 +398,7 @@ class Client(object): hsts = config.hsts if "ensure-http-header" in supported else False uir = config.uir if "ensure-http-header" in supported else False staple = config.staple if "staple-ocsp" in supported else False + must_staple = config.must_staple if redirect is None: redirect = enhancements.ask("redirect") @@ -411,11 +412,11 @@ class Client(object): if uir: self.apply_enhancement(domains, "ensure-http-header", "Upgrade-Insecure-Requests") - if staple: + if staple or must_staple: self.apply_enhancement(domains, "staple-ocsp") msg = ("We were unable to restart web server") - if redirect or hsts or uir or staple: + if redirect or hsts or uir or staple or must_staple: with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart() diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 8e8666e70..19d9f0c07 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -201,9 +201,9 @@ class IConfig(zope.interface.Interface): "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") must_staple = zope.interface.Attribute( - "Whether to request the OCSP Must Staple certificate extension. " - "Additional setup may be required after issuance. This does not " - "currently autoconfigure web servers for OCSP stapling. ") + "Adds the OCSP Must Staple extension to the certificate." + "Autoconfigures OCSP Stapling for supported setups " + "(Apache version >= 2.3.3 ).") config_dir = zope.interface.Attribute("Configuration directory.") work_dir = zope.interface.Attribute("Working directory.") From 0999705691ae6040ae6dd7221c6069dd12cfd9f0 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 02:15:50 +0200 Subject: [PATCH 090/192] Fixing wrapping for messages with URLs --- certbot/display/util.py | 23 ++++++++++++++++++----- certbot/reporter.py | 9 +++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 8de607534..b4004997f 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -41,10 +41,15 @@ def _wrap_lines(msg): """ lines = msg.splitlines() fixed_l = [] - for line in lines: - fixed_l.append(textwrap.fill(line, 80)) - return os.linesep.join(fixed_l) + for line in lines: + fixed_l.append(textwrap.fill( + line, + 80, + break_long_words=False, + break_on_hyphens=False)) + + return os.linesep.join(fixed_l) @zope.interface.implementer(interfaces.IDisplay) class NcursesDisplay(object): @@ -265,7 +270,11 @@ class FileDisplay(object): """ ans = raw_input( - textwrap.fill("%s (Enter 'c' to cancel): " % message, 80)) + textwrap.fill( + "%s (Enter 'c' to cancel): " % message, + 80, + break_long_words=False, + break_on_hyphens=False)) if ans == "c" or ans == "C": return CANCEL, "-1" @@ -402,7 +411,11 @@ 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)) + textwrap.fill( + "{num}: {desc}".format(num=i, desc=desc), + 80, + break_long_words=False, + break_on_hyphens=False)) # Keep this outside of the textwrap self.outfile.write(os.linesep) diff --git a/certbot/reporter.py b/certbot/reporter.py index d509cb0b8..43e8cd0dc 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -82,10 +82,15 @@ class Reporter(object): print(le_util.ANSI_SGR_BOLD) print('IMPORTANT NOTES:') first_wrapper = textwrap.TextWrapper( - initial_indent=' - ', subsequent_indent=(' ' * 3)) + initial_indent=' - ', + subsequent_indent=(' ' * 3), + break_long_words=False, + break_on_hyphens=False) next_wrapper = textwrap.TextWrapper( initial_indent=first_wrapper.subsequent_indent, - subsequent_indent=first_wrapper.subsequent_indent) + subsequent_indent=first_wrapper.subsequent_indent, + break_long_words=False, + break_on_hyphens=False) while not self.messages.empty(): msg = self.messages.get() if self.config.quiet: From 5a3397cf63ac0a4d0d5196e6bf20c50ec3356f96 Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 21:07:47 +0000 Subject: [PATCH 091/192] Fix tests --- certbot/tests/client_test.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 8490efd9f..e1aea1b64 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -306,7 +306,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -322,7 +322,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config_no_ask(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -331,16 +331,16 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.supported_enhancements.return_value = ["redirect", "ensure-http-header"] - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "redirect", None) - config = ConfigHelper(redirect=False, hsts=True, uir=False) + config = ConfigHelper(redirect=False, hsts=True, uir=False, must_staple=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Strict-Transport-Security") - config = ConfigHelper(redirect=False, hsts=False, uir=True) + config = ConfigHelper(redirect=False, hsts=False, uir=True, must_staple=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Upgrade-Insecure-Requests") @@ -354,13 +354,13 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.supported_enhancements.return_value = [] - config = ConfigHelper(redirect=None, hsts=True, uir=True) + config = ConfigHelper(redirect=None, hsts=True, uir=True, must_staple=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_not_called() mock_enhancements.ask.assert_not_called() def test_enhance_config_no_installer(self): - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -374,7 +374,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.enhance.side_effect = errors.PluginError - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -391,7 +391,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.save.side_effect = errors.PluginError - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -408,7 +408,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.restart.side_effect = [errors.PluginError, None] - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -428,7 +428,7 @@ class ClientTest(unittest.TestCase): installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError - config = ConfigHelper(redirect=True, hsts=False, uir=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) From 911b4565bea1cb4dbcfc49a4c4ce4337c377e207 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 23:09:17 +0200 Subject: [PATCH 092/192] Moving check for conflicting args to cli.py --- certbot/cli.py | 10 ++++++++++ certbot/main.py | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 44712ada0..72ec94915 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -343,6 +343,16 @@ class HelpfulArgumentParser(object): if parsed_args.csr: self.handle_csr(parsed_args) + # Avoid conflicting args + conficting_args = ["quiet", "noninteractive_mode", "text_mode"] + if parsed_args.dialog_mode: + for arg in conficting_args: + if getattr(parsed_args, arg): + raise errors.Error( + ("Conflicting values for displayer." + " {0} conflicts with dialog_mode").format(arg) + ) + hooks.validate_hooks(parsed_args) return parsed_args diff --git a/certbot/main.py b/certbot/main.py index 99d06a69e..4ef2e6ac8 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -673,16 +673,6 @@ def main(cli_args=sys.argv[1:]): sys.excepthook = functools.partial(_handle_exception, config=config) - # Avoid conflicting args - conficting_args = ["quiet", "noninteractive_mode", "text_mode"] - if config.dialog_mode: - for arg in conficting_args: - if getattr(config, arg): - raise errors.Error( - ("Conflicting values for displayer." - " {0} conflicts with dialog_mode").format(arg) - ) - # Displayer if config.quiet: config.noninteractive_mode = True From 0b691d1b561c0bb5b068664935fb4fe4ed19dbdc Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 25 May 2016 14:11:12 -0700 Subject: [PATCH 093/192] Use better update_registration invocation, and reporter interface --- certbot/main.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index fa5797483..3a3218a49 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -10,7 +10,6 @@ import traceback import zope.component -from acme import errors as acme_errors from acme import jose import certbot @@ -397,17 +396,12 @@ def register(config, unused_plugins): "required\n(hint: --email)") acc, acme = _determine_account(config) acme_client = client.Client(config, acc, None, None, acme=acme) - try: - updated_reg = client.messages.Registration.from_data(email=config.email) - acme_client.acme.update_registration(acme_client.account.regr, - updated_reg) - except acme_errors.UnexpectedUpdate: - # We expect the unexpected update! - pass - query_data = acme_client.acme.query_registration(acme_client.account.regr) - # We rely on an ACME exception to interrupt this process if it didn't work. - print("Registration change succeeded. New registration data:\n") - print(query_data) + data = acme_client.acme.update_registration(acc.regr.update( + body=acc.regr.body.update(contact=('mailto:' + config.email,)))) + # We rely on an exception to interrupt this process if it didn't work. + reporter_util = zope.component.getUtility(interfaces.IReporter) + msg = "Your e-mail address was updated to {0}.".format(config.email) + reporter_util.add_message(msg, reporter_util.HIGH_PRIORITY) return From 6ceaca4a9d26c87c732329b149a7f7301fe22ad0 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Wed, 25 May 2016 14:11:43 -0700 Subject: [PATCH 094/192] Minor test updates for update_registration call change --- certbot/tests/cli_test.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 4dfc78792..007675759 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -947,25 +947,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_storage = mock.MagicMock() mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] - mocked_det.return_value = ("a", "b") + mocked_det.return_value = (mock.MagicMock(), "foo") acme_client = mock.MagicMock() mocked_client.Client.return_value = acme_client - # Currently the update_registration() call always - # raises a harmless acme_errors.UnexpectedUpdate. - # If this is fixed, we should get rid of both this - # side effect and the corresponding try/catch in - # main.register(). - uu = acme_errors.UnexpectedUpdate - acme_client.acme.update_registration.side_effect = uu x = self._call_no_clientmock( ["register", "--update-registration", "--email", "user@example.org"]) # When registration change succeeds, the return value # of register() is None assert x[0] is None - # and we got far enough to query the registration from + # and we got supposedly did update the registration from # the server - assert acme_client.acme.query_registration.call_count == 1 + assert acme_client.acme.update_registration.call_count == 1 class DetermineAccountTest(unittest.TestCase): From 74f5c851782cf40b938f4e6389596d6c994d2f57 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 23:13:26 +0200 Subject: [PATCH 095/192] Moving tests for conflicting args to cli_test.py --- certbot/tests/cli_test.py | 4 ++++ certbot/tests/main_test.py | 39 +++----------------------------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index afaa53570..20d891324 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -908,6 +908,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._call(['-c', test_util.vector_path('cli.ini')]) self.assertTrue(mocked_run.called) + def test_conflicting_args(self): + args = ['renew', '--dialog', '--text'] + self.assertRaises(errors.Error, self._call, args) + class DetermineAccountTest(unittest.TestCase): """Tests for certbot.cli._determine_account.""" diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index e3f8b860d..66cba64a3 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1,49 +1,15 @@ """Tests for certbot.main.""" -import os -import shutil -import tempfile import unittest + import mock -import six + from certbot import cli from certbot import configuration -from certbot import errors -from certbot import main from certbot.plugins import disco as plugins_disco -class MainTest(unittest.TestCase): - """Tests for certbot.main""" - - def setUp(self): - self.tmp_dir = tempfile.mkdtemp() - self.config_dir = os.path.join(self.tmp_dir, 'config') - self.work_dir = os.path.join(self.tmp_dir, 'work') - self.logs_dir = os.path.join(self.tmp_dir, 'logs') - self.standard_args = ['--config-dir', self.config_dir, - '--work-dir', self.work_dir, - '--logs-dir', self.logs_dir, '--text'] - - def tearDown(self): - shutil.rmtree(self.tmp_dir) - - def _call(self, args, stdout=None): - "Run the client with output streams mocked out" - args = self.standard_args + args - - toy_stdout = stdout if stdout else six.StringIO() - with mock.patch('certbot.main.sys.stdout', new=toy_stdout): - with mock.patch('certbot.main.sys.stderr') as stderr: - ret = main.main(args[:]) # NOTE: parser can alter its args! - return ret, toy_stdout, stderr - - def test_args_conflict(self): - args = ['renew', '--dialog', '--text'] - self.assertRaises(errors.Error, self._call, args) - - class ObtainCertTest(unittest.TestCase): """Tests for certbot.main.obtain_cert.""" @@ -60,6 +26,7 @@ class ObtainCertTest(unittest.TestCase): config = configuration.NamespaceConfig( cli.prepare_and_parse_args(plugins, args)) + from certbot import main with mock.patch('certbot.main._init_le_client') as mock_init: main.obtain_cert(config, plugins) From efcd0090da49c9649b835d1bd5e3350f17b19fcc Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 21:20:13 +0000 Subject: [PATCH 096/192] Add a specific must-staple test --- certbot/tests/client_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index e1aea1b64..ae66ab5d0 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -304,6 +304,25 @@ class ClientTest(unittest.TestCase): installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) + @mock.patch("certbot.client.enhancements") + def test_must_staple(self, mock_enhancements): + # Testing our wanted behaviour: Enable OCSP Stapling when the + # --must-staple flag is on. [Without having the --staple-ocsp flag on] + config = ConfigHelper(must_staple=True, staple=False) + self.assertRaises(errors.Error, + self.client.enhance_config, ["foo.bar"], config) + + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + installer.supported_enhancements.return_value = ["staple-ocsp"] + + self.client.enhance_config(["foo.bar"], config) + installer.enhance.assert_called_once_with("foo.bar", "staple-ocsp", None) + self.assertEqual(installer.save.call_count, 1) + installer.restart.assert_called_once_with() + + @mock.patch("certbot.client.enhancements") def test_enhance_config(self, mock_enhancements): config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) From b77d288adb0f78db6032a1d066a546d3c99ca9b0 Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 21:49:53 +0000 Subject: [PATCH 097/192] Use cli.py to set .staple given .must_staple --- certbot/cli.py | 3 +++ certbot/tests/cli_test.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index 3bd565496..05a189712 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -343,6 +343,9 @@ class HelpfulArgumentParser(object): if parsed_args.csr: self.handle_csr(parsed_args) + if parsed_args.must_staple: + parsed_args.staple = True + hooks.validate_hooks(parsed_args) return parsed_args diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 4ae69e69d..00c9a0a26 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -422,6 +422,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods for arg in conflicting_args: self.assertTrue(arg in error.message) + def test_must_staple_flag(self): + parse = self._get_argument_parser() + short_args = ['--must-staple'] + namespace = parse(short_args) + self.assertTrue(namespace.must_staple) + self.assertTrue(namespace.staple) + def test_staging_flag(self): parse = self._get_argument_parser() short_args = ['--staging'] From 2268dbf48987e902bf01580d3748853afc6b2778 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 14:55:44 -0700 Subject: [PATCH 098/192] delint --- certbot/main.py | 2 +- certbot/tests/cli_test.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 3a3218a49..b7dc88839 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -396,7 +396,7 @@ def register(config, unused_plugins): "required\n(hint: --email)") acc, acme = _determine_account(config) acme_client = client.Client(config, acc, None, None, acme=acme) - data = acme_client.acme.update_registration(acc.regr.update( + acme_client.acme.update_registration(acc.regr.update( body=acc.regr.body.update(contact=('mailto:' + config.email,)))) # We rely on an exception to interrupt this process if it didn't work. reporter_util = zope.component.getUtility(interfaces.IReporter) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 007675759..92caf8f04 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -14,7 +14,6 @@ import mock import six from six.moves import reload_module # pylint: disable=import-error -from acme import errors as acme_errors from acme import jose from certbot import account From 20be730a924e3a32c005556ce5a62ac1e279f1d0 Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 21:56:15 +0000 Subject: [PATCH 099/192] Revert client, client_test back --- certbot/client.py | 5 ++--- certbot/tests/client_test.py | 41 ++++++++++-------------------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 5d1338baf..ba31f8760 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -406,7 +406,6 @@ class Client(object): hsts = config.hsts if "ensure-http-header" in supported else False uir = config.uir if "ensure-http-header" in supported else False staple = config.staple if "staple-ocsp" in supported else False - must_staple = config.must_staple if redirect is None: redirect = enhancements.ask("redirect") @@ -420,11 +419,11 @@ class Client(object): if uir: self.apply_enhancement(domains, "ensure-http-header", "Upgrade-Insecure-Requests") - if staple or must_staple: + if staple: self.apply_enhancement(domains, "staple-ocsp") msg = ("We were unable to restart web server") - if redirect or hsts or uir or staple or must_staple: + if redirect or hsts or uir or staple: with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart() diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index ae66ab5d0..8490efd9f 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -304,28 +304,9 @@ class ClientTest(unittest.TestCase): installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) - @mock.patch("certbot.client.enhancements") - def test_must_staple(self, mock_enhancements): - # Testing our wanted behaviour: Enable OCSP Stapling when the - # --must-staple flag is on. [Without having the --staple-ocsp flag on] - config = ConfigHelper(must_staple=True, staple=False) - self.assertRaises(errors.Error, - self.client.enhance_config, ["foo.bar"], config) - - mock_enhancements.ask.return_value = True - installer = mock.MagicMock() - self.client.installer = installer - installer.supported_enhancements.return_value = ["staple-ocsp"] - - self.client.enhance_config(["foo.bar"], config) - installer.enhance.assert_called_once_with("foo.bar", "staple-ocsp", None) - self.assertEqual(installer.save.call_count, 1) - installer.restart.assert_called_once_with() - - @mock.patch("certbot.client.enhancements") def test_enhance_config(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -341,7 +322,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.enhancements") def test_enhance_config_no_ask(self, mock_enhancements): - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -350,16 +331,16 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.supported_enhancements.return_value = ["redirect", "ensure-http-header"] - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "redirect", None) - config = ConfigHelper(redirect=False, hsts=True, uir=False, must_staple=False) + config = ConfigHelper(redirect=False, hsts=True, uir=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Strict-Transport-Security") - config = ConfigHelper(redirect=False, hsts=False, uir=True, must_staple=False) + config = ConfigHelper(redirect=False, hsts=False, uir=True) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Upgrade-Insecure-Requests") @@ -373,13 +354,13 @@ class ClientTest(unittest.TestCase): self.client.installer = installer installer.supported_enhancements.return_value = [] - config = ConfigHelper(redirect=None, hsts=True, uir=True, must_staple=False) + config = ConfigHelper(redirect=None, hsts=True, uir=True) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_not_called() mock_enhancements.ask.assert_not_called() def test_enhance_config_no_installer(self): - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @@ -393,7 +374,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.enhance.side_effect = errors.PluginError - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -410,7 +391,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.save.side_effect = errors.PluginError - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -427,7 +408,7 @@ class ClientTest(unittest.TestCase): installer.supported_enhancements.return_value = ["redirect"] installer.restart.side_effect = [errors.PluginError, None] - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) @@ -447,7 +428,7 @@ class ClientTest(unittest.TestCase): installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError - config = ConfigHelper(redirect=True, hsts=False, uir=False, must_staple=False) + config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) From 29822aad9d2a76780178d2e1f493ef4b5966d40f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 14:57:01 -0700 Subject: [PATCH 100/192] denit --- certbot/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index b7dc88839..56d09c078 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -401,8 +401,7 @@ def register(config, unused_plugins): # We rely on an exception to interrupt this process if it didn't work. reporter_util = zope.component.getUtility(interfaces.IReporter) msg = "Your e-mail address was updated to {0}.".format(config.email) - reporter_util.add_message(msg, reporter_util.HIGH_PRIORITY) - return + reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) def install(config, plugins): From d57353a6fea562b00cc3e37e71b76e12ff380fef Mon Sep 17 00:00:00 2001 From: sagi Date: Wed, 25 May 2016 22:01:43 +0000 Subject: [PATCH 101/192] Add missing space. --- certbot/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 19d9f0c07..37835462e 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -201,7 +201,7 @@ class IConfig(zope.interface.Interface): "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") must_staple = zope.interface.Attribute( - "Adds the OCSP Must Staple extension to the certificate." + "Adds the OCSP Must Staple extension to the certificate. " "Autoconfigures OCSP Stapling for supported setups " "(Apache version >= 2.3.3 ).") From 94588b1a9175af5f5943bfb8b9485c3bab376a5c Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 25 May 2016 14:43:10 -0700 Subject: [PATCH 102/192] Check out a specific version of Boulder. A recent Boulder change broke integration tests, this fixes it. --- tests/boulder-fetch.sh | 2 +- tests/letstest/scripts/boulder_install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index a09d0adf9..01f236575 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -3,7 +3,7 @@ set -xe # Check out special branch until latest docker changes land in Boulder master. -git clone https://github.com/letsencrypt/boulder $BOULDERPATH +git clone -b 71e4af43f792f33e6ab1aa87ddc23a6a368889f2 https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d diff --git a/tests/letstest/scripts/boulder_install.sh b/tests/letstest/scripts/boulder_install.sh index 426642880..0d6153a2d 100755 --- a/tests/letstest/scripts/boulder_install.sh +++ b/tests/letstest/scripts/boulder_install.sh @@ -3,7 +3,7 @@ # >>>> only tested on Ubuntu 14.04LTS <<<< # Check out special branch until latest docker changes land in Boulder master. -git clone https://github.com/letsencrypt/boulder $BOULDERPATH +git clone -b 71e4af43f792f33e6ab1aa87ddc23a6a368889f2 https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d From 0fb3704dcedf66f67b517b22833564f00cf74c48 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 25 May 2016 15:43:54 -0700 Subject: [PATCH 103/192] Use a real branch name. --- tests/boulder-fetch.sh | 2 +- tests/letstest/scripts/boulder_install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index 01f236575..469c5cd80 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -3,7 +3,7 @@ set -xe # Check out special branch until latest docker changes land in Boulder master. -git clone -b 71e4af43f792f33e6ab1aa87ddc23a6a368889f2 https://github.com/letsencrypt/boulder $BOULDERPATH +git clone -b docker-integration https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d diff --git a/tests/letstest/scripts/boulder_install.sh b/tests/letstest/scripts/boulder_install.sh index 0d6153a2d..7e298783f 100755 --- a/tests/letstest/scripts/boulder_install.sh +++ b/tests/letstest/scripts/boulder_install.sh @@ -3,7 +3,7 @@ # >>>> only tested on Ubuntu 14.04LTS <<<< # Check out special branch until latest docker changes land in Boulder master. -git clone -b 71e4af43f792f33e6ab1aa87ddc23a6a368889f2 https://github.com/letsencrypt/boulder $BOULDERPATH +git clone -b docker-integration https://github.com/letsencrypt/boulder $BOULDERPATH cd $BOULDERPATH sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml docker-compose up -d From 14e2ea92ee21fa1bea1b7b5ae07b47a7a29dade0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:32:03 -0700 Subject: [PATCH 104/192] Add a way to only save registration resources --- certbot/account.py | 23 ++++++++++++++++++----- certbot/tests/account_test.py | 10 ++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/certbot/account.py b/certbot/account.py index cc50a6ea6..5e0d4f2fd 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -186,16 +186,29 @@ class AccountFileStorage(interfaces.AccountStorage): return acc def save(self, account): + self._save(account, regr_only=False) + + def save_regr(self, account): + """Save the registration resource. + + :param Account account: account whose regr should be saved + + """ + self._save(account, regr_only=True) + + def _save(self, account, regr_only): account_dir_path = self._account_dir_path(account.id) le_util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), self.config.strict_permissions) try: with open(self._regr_path(account_dir_path), "w") as regr_file: regr_file.write(account.regr.json_dumps()) - with le_util.safe_open(self._key_path(account_dir_path), - "w", chmod=0o400) as key_file: - key_file.write(account.key.json_dumps()) - with open(self._metadata_path(account_dir_path), "w") as metadata_file: - metadata_file.write(account.meta.json_dumps()) + if not regr_only: + with le_util.safe_open(self._key_path(account_dir_path), + "w", chmod=0o400) as key_file: + key_file.write(account.key.json_dumps()) + with open(self._metadata_path( + account_dir_path), "w") as metadata_file: + metadata_file.write(account.meta.json_dumps()) except IOError as error: raise errors.AccountStorageError(error) diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index a96e57507..4cd2bfebf 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -137,6 +137,16 @@ class AccountFileStorageTest(unittest.TestCase): # restore self.assertEqual(self.acc, self.storage.load(self.acc.id)) + def test_save_regr(self): + self.storage.save_regr(self.acc) + account_path = os.path.join(self.config.accounts_dir, self.acc.id) + self.assertTrue(os.path.exists(account_path)) + self.assertTrue(os.path.exists(os.path.join( + account_path, "regr.json"))) + for file_name in "meta.json", "private_key.json": + self.assertFalse(os.path.exists( + os.path.join(account_path, file_name))) + def test_find_all(self): self.storage.save(self.acc) self.assertEqual([self.acc], self.storage.find_all()) From 5531c156e8511c0528101452c7a5f71d3952175f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:41:19 -0700 Subject: [PATCH 105/192] Save the updated registration --- certbot/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 56d09c078..3491f44a6 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -396,9 +396,10 @@ def register(config, unused_plugins): "required\n(hint: --email)") acc, acme = _determine_account(config) acme_client = client.Client(config, acc, None, None, acme=acme) - acme_client.acme.update_registration(acc.regr.update( - body=acc.regr.body.update(contact=('mailto:' + config.email,)))) # We rely on an exception to interrupt this process if it didn't work. + acc.regr = acme_client.acme.update_registration(acc.regr.update( + body=acc.regr.body.update(contact=('mailto:' + config.email,)))) + account_storage.save_regr(account) reporter_util = zope.component.getUtility(interfaces.IReporter) msg = "Your e-mail address was updated to {0}.".format(config.email) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) From a87df33de69c9105888e8ec857e36af5c000b58d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:41:39 -0700 Subject: [PATCH 106/192] Update register tests --- certbot/tests/cli_test.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 92caf8f04..fbb822490 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -914,7 +914,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] x = self._call_no_clientmock(["register", "--email", "user@example.org"]) - assert "There is an existing account" in x[0] + self.assertTrue("There is an existing account" in x[0]) def test_update_registration_no_existing_accounts(self): # with mock.patch('certbot.main.client') as mocked_client: @@ -924,8 +924,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_storage.find_all.return_value = [] x = self._call_no_clientmock( ["register", "--update-registration", "--email", - "user@example.org"]) - assert "Could not find an existing account" in x[0] + "user@example.org"]) + self.assertTrue("Could not find an existing account" in x[0]) def test_update_registration_no_email(self): # This test will become obsolete when register --update-registration @@ -936,7 +936,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] x = self._call_no_clientmock(["register", "--update-registration"]) - assert "can only change the e-mail" in x[0] + self.assertTrue("can only change the e-mail" in x[0]) def test_update_registration_with_email(self): with mock.patch('certbot.main.client') as mocked_client: @@ -951,13 +951,16 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mocked_client.Client.return_value = acme_client x = self._call_no_clientmock( ["register", "--update-registration", "--email", - "user@example.org"]) + "user@example.org"]) # When registration change succeeds, the return value # of register() is None - assert x[0] is None + self.assertTrue(x[0] is None) # and we got supposedly did update the registration from # the server - assert acme_client.acme.update_registration.call_count == 1 + self.assertTrue( + acme_client.acme.update_registration.called) + # and we saved the updated registration on disk + self.assertTrue(mocked_storage.save_regr.called) class DetermineAccountTest(unittest.TestCase): From 1819b22ebc11b917cc59ea503001596657f00234 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:47:05 -0700 Subject: [PATCH 107/192] Fix typo --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 3491f44a6..20b9a7ce5 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -399,7 +399,7 @@ def register(config, unused_plugins): # We rely on an exception to interrupt this process if it didn't work. acc.regr = acme_client.acme.update_registration(acc.regr.update( body=acc.regr.body.update(contact=('mailto:' + config.email,)))) - account_storage.save_regr(account) + account_storage.save_regr(acc) reporter_util = zope.component.getUtility(interfaces.IReporter) msg = "Your e-mail address was updated to {0}.".format(config.email) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) From d9d23772422153a58328556b305e626fe61ac898 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 25 May 2016 18:28:40 -0500 Subject: [PATCH 108/192] Rename certbot.le_util to certbot.util Also rename certbot/tests/le_util_test.py to certbot/tests/util_test.py --- certbot-apache/certbot_apache/configurator.py | 19 +++--- certbot-apache/certbot_apache/constants.py | 4 +- certbot-nginx/certbot_nginx/configurator.py | 14 ++-- certbot/account.py | 8 +-- certbot/cli.py | 6 +- certbot/client.py | 22 +++--- certbot/colored_logging.py | 4 +- certbot/configuration.py | 4 +- certbot/crypto_util.py | 27 ++++---- certbot/display/ops.py | 8 +-- certbot/main.py | 8 +-- certbot/plugins/common.py | 4 +- certbot/plugins/common_test.py | 2 +- certbot/renewal.py | 4 +- certbot/reporter.py | 8 +-- certbot/reverter.py | 10 +-- certbot/storage.py | 8 +-- certbot/tests/auth_handler_test.py | 4 +- certbot/tests/cli_test.py | 8 +-- certbot/tests/client_test.py | 6 +- certbot/tests/colored_logging_test.py | 6 +- certbot/tests/crypto_util_test.py | 16 ++--- certbot/tests/display/ops_test.py | 6 +- certbot/tests/reverter_test.py | 2 +- certbot/tests/storage_test.py | 2 +- .../tests/{le_util_test.py => util_test.py} | 68 +++++++++---------- certbot/{le_util.py => util.py} | 0 27 files changed, 138 insertions(+), 140 deletions(-) rename certbot/tests/{le_util_test.py => util_test.py} (85%) rename certbot/{le_util.py => util.py} (100%) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 1e02ae7b3..e4c06ba7e 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -15,7 +15,7 @@ from acme import challenges from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.plugins import common @@ -106,8 +106,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): add("handle-sites", default=constants.os_constant("handle_sites"), help="Let installer handle enabling sites for you." + "(Only Ubuntu/Debian currently)") - le_util.add_deprecated_argument(add, argument_name="ctl", nargs=1) - le_util.add_deprecated_argument( + util.add_deprecated_argument(add, argument_name="ctl", nargs=1) + util.add_deprecated_argument( add, argument_name="init-script", nargs=1) def __init__(self, *args, **kwargs): @@ -151,7 +151,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ # Verify Apache is installed - if not le_util.exe_exists(constants.os_constant("restart_cmd")[0]): + if not util.exe_exists(constants.os_constant("restart_cmd")[0]): raise errors.NoInstallationError # Make sure configuration is valid @@ -1521,14 +1521,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Generate reversal command. # Try to be safe here... check that we can probably reverse before # applying enmod command - if not le_util.exe_exists(self.conf("dismod")): + if not util.exe_exists(self.conf("dismod")): raise errors.MisconfigurationError( "Unable to find a2dismod, please make sure a2enmod and " "a2dismod are configured correctly for certbot.") self.reverter.register_undo_command( temp, [self.conf("dismod"), mod_name]) - le_util.run_script([self.conf("enmod"), mod_name]) + util.run_script([self.conf("enmod"), mod_name]) def restart(self): """Runs a config test and reloads the Apache server. @@ -1547,7 +1547,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - le_util.run_script(constants.os_constant("restart_cmd")) + util.run_script(constants.os_constant("restart_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1558,7 +1558,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - le_util.run_script(constants.os_constant("conftest_cmd")) + util.run_script(constants.os_constant("conftest_cmd")) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -1574,8 +1574,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: - stdout, _ = le_util.run_script( - constants.os_constant("version_cmd")) + stdout, _ = util.run_script(constants.os_constant("version_cmd")) except errors.SubprocessError: raise errors.PluginError( "Unable to run %s -v" % diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index f3226572c..faf74394d 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -1,6 +1,6 @@ """Apache plugin constants.""" import pkg_resources -from certbot import le_util +from certbot import util CLI_DEFAULTS_DEBIAN = dict( @@ -116,7 +116,7 @@ def os_constant(key): :param key: name of cli constant :return: value of constant for active os """ - os_info = le_util.get_os_info() + os_info = util.get_os_info() try: constants = CLI_DEFAULTS[os_info[0].lower()] except KeyError: diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index e402d5c79..30928e56c 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -17,7 +17,7 @@ from certbot import constants as core_constants from certbot import crypto_util from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import reverter from certbot.plugins import common @@ -111,7 +111,7 @@ class NginxConfigurator(common.Plugin): :raises .errors.MisconfigurationError: If Nginx is misconfigured """ # Verify Nginx is installed - if not le_util.exe_exists(self.conf('ctl')): + if not util.exe_exists(self.conf('ctl')): raise errors.NoInstallationError # Make sure configuration is valid @@ -318,7 +318,7 @@ class NginxConfigurator(common.Plugin): cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()]) cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) - cert_file, cert_path = le_util.unique_file(os.path.join(tmp_dir, "cert.pem")) + cert_file, cert_path = util.unique_file(os.path.join(tmp_dir, "cert.pem")) with cert_file: cert_file.write(cert_pem) return cert_path, le_key.file @@ -426,7 +426,7 @@ class NginxConfigurator(common.Plugin): """ try: - le_util.run_script([self.conf('ctl'), "-c", self.nginx_conf, "-t"]) + util.run_script([self.conf('ctl'), "-c", self.nginx_conf, "-t"]) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) @@ -439,11 +439,11 @@ class NginxConfigurator(common.Plugin): """ uid = os.geteuid() - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.work_dir, core_constants.CONFIG_DIRS_MODE, uid) - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.backup_dir, core_constants.CONFIG_DIRS_MODE, uid) - le_util.make_or_verify_dir( + util.make_or_verify_dir( self.config.config_dir, core_constants.CONFIG_DIRS_MODE, uid) def get_version(self): diff --git a/certbot/account.py b/certbot/account.py index cc50a6ea6..2ef3629e2 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -16,7 +16,7 @@ from acme import messages from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) @@ -130,7 +130,7 @@ class AccountFileStorage(interfaces.AccountStorage): """ def __init__(self, config): self.config = config - le_util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(), + util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(), self.config.strict_permissions) def _account_dir_path(self, account_id): @@ -187,12 +187,12 @@ class AccountFileStorage(interfaces.AccountStorage): def save(self, account): account_dir_path = self._account_dir_path(account.id) - le_util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), + util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), self.config.strict_permissions) try: with open(self._regr_path(account_dir_path), "w") as regr_file: regr_file.write(account.regr.json_dumps()) - with le_util.safe_open(self._key_path(account_dir_path), + with util.safe_open(self._key_path(account_dir_path), "w", chmod=0o400) as key_file: key_file.write(account.key.json_dumps()) with open(self._metadata_path(account_dir_path), "w") as metadata_file: diff --git a/certbot/cli.py b/certbot/cli.py index 05a189712..e8f4a16f0 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -17,7 +17,7 @@ from certbot import crypto_util from certbot import errors from certbot import hooks from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.plugins import disco as plugins_disco import certbot.plugins.selection as plugin_selection @@ -505,7 +505,7 @@ class HelpfulArgumentParser(object): :param int nargs: Number of arguments the option takes. """ - le_util.add_deprecated_argument( + util.add_deprecated_argument( self.parser.add_argument, argument_name, num_args) def add_group(self, topic, **kwargs): @@ -938,7 +938,7 @@ def add_domains(args_or_config, domains): """ validated_domains = [] for domain in domains.split(","): - domain = le_util.enforce_domain_sanity(domain.strip()) + domain = util.enforce_domain_sanity(domain.strip()) validated_domains.append(domain) if domain not in args_or_config.domains: args_or_config.domains.append(domain) diff --git a/certbot/client.py b/certbot/client.py index ba31f8760..3d17e2295 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -21,7 +21,7 @@ from certbot import crypto_util from certbot import errors from certbot import error_handler from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import reverter from certbot import storage from certbot import cli @@ -53,7 +53,7 @@ def _determine_user_agent(config): if config.user_agent is None: ua = "CertbotACMEClient/{0} ({1}) Authenticator/{2} Installer/{3}" - ua = ua.format(certbot.__version__, " ".join(le_util.get_os_info()), + ua = ua.format(certbot.__version__, " ".join(util.get_os_info()), config.authenticator, config.installer) else: ua = config.user_agent @@ -198,7 +198,7 @@ class Client(object): consistent with identifiers present in the `csr`. :param list domains: Domain names. - :param .le_util.CSR csr: DER-encoded Certificate Signing + :param .util.CSR csr: DER-encoded Certificate Signing Request. The key used to generate this CSR can be different than `authkey`. :param list authzr: List of @@ -237,8 +237,8 @@ class Client(object): :returns: `.CertificateResource`, certificate chain (as returned by `.fetch_chain`), and newly generated private key - (`.le_util.Key`) and DER-encoded Certificate Signing Request - (`.le_util.CSR`). + (`.util.Key`) and DER-encoded Certificate Signing Request + (`.util.CSR`). :rtype: tuple """ @@ -312,7 +312,7 @@ class Client(object): """ for path in cert_path, chain_path, fullchain_path: - le_util.make_or_verify_dir( + util.make_or_verify_dir( os.path.dirname(path), 0o755, os.geteuid(), self.config.strict_permissions) @@ -504,9 +504,9 @@ def validate_key_csr(privkey, csr=None): If csr is left as None, only the key will be validated. :param privkey: Key associated with CSR - :type privkey: :class:`certbot.le_util.Key` + :type privkey: :class:`certbot.util.Key` - :param .le_util.CSR csr: CSR + :param .util.CSR csr: CSR :raises .errors.Error: when validation fails @@ -523,7 +523,7 @@ def validate_key_csr(privkey, csr=None): if csr.form == "der": csr_obj = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, csr.data) - csr = le_util.CSR(csr.file, OpenSSL.crypto.dump_certificate( + csr = util.CSR(csr.file, OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, csr_obj), "pem") # If CSR is provided, it must be readable and valid. @@ -586,10 +586,10 @@ def _open_pem_file(cli_arg_path, pem_path): """ if cli.set_by_cli(cli_arg_path): - return le_util.safe_open(pem_path, chmod=0o644),\ + return util.safe_open(pem_path, chmod=0o644),\ os.path.abspath(pem_path) else: - uniq = le_util.unique_file(pem_path, 0o644) + uniq = util.unique_file(pem_path, 0o644) return uniq[0], os.path.abspath(uniq[1]) def _save_chain(chain_pem, chain_file): diff --git a/certbot/colored_logging.py b/certbot/colored_logging.py index d42fb5966..93bf3a55a 100644 --- a/certbot/colored_logging.py +++ b/certbot/colored_logging.py @@ -2,7 +2,7 @@ import logging import sys -from certbot import le_util +from certbot import util class StreamHandler(logging.StreamHandler): @@ -40,6 +40,6 @@ class StreamHandler(logging.StreamHandler): if sys.version_info < (2, 7) else super(StreamHandler, self).format(record)) if self.colored and record.levelno >= self.red_level: - return ''.join((le_util.ANSI_SGR_RED, out, le_util.ANSI_SGR_RESET)) + return ''.join((util.ANSI_SGR_RED, out, util.ANSI_SGR_RESET)) else: return out diff --git a/certbot/configuration.py b/certbot/configuration.py index 172b35bfe..712135b8d 100644 --- a/certbot/configuration.py +++ b/certbot/configuration.py @@ -8,7 +8,7 @@ import zope.interface from certbot import constants from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util @zope.interface.implementer(interfaces.IConfig) @@ -132,4 +132,4 @@ def check_config_sanity(config): if config.namespace.domains is not None: for domain in config.namespace.domains: # This may be redundant, but let's be paranoid - le_util.enforce_domain_sanity(domain) + util.enforce_domain_sanity(domain) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 68e07e059..6b1b8426c 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -17,7 +17,7 @@ from acme import jose from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): :param str keyname: Filename of key :returns: Key - :rtype: :class:`certbot.le_util.Key` + :rtype: :class:`certbot.util.Key` :raises ValueError: If unable to generate the key given key_size. @@ -50,30 +50,29 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"): config = zope.component.getUtility(interfaces.IConfig) # Save file - le_util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), - config.strict_permissions) - key_f, key_path = le_util.unique_file( - os.path.join(key_dir, keyname), 0o600) + util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), + config.strict_permissions) + key_f, key_path = util.unique_file(os.path.join(key_dir, keyname), 0o600) with key_f: key_f.write(key_pem) logger.info("Generating key (%d bits): %s", key_size, key_path) - return le_util.Key(key_path, key_pem) + return util.Key(key_path, key_pem) def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"): """Initialize a CSR with the given private key. :param privkey: Key to include in the CSR - :type privkey: :class:`certbot.le_util.Key` + :type privkey: :class:`certbot.util.Key` :param set names: `str` names to include in the CSR :param str path: Certificate save directory. :returns: CSR - :rtype: :class:`certbot.le_util.CSR` + :rtype: :class:`certbot.util.CSR` """ config = zope.component.getUtility(interfaces.IConfig) @@ -82,16 +81,16 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"): must_staple=config.must_staple) # Save CSR - le_util.make_or_verify_dir(path, 0o755, os.geteuid(), + util.make_or_verify_dir(path, 0o755, os.geteuid(), config.strict_permissions) - csr_f, csr_filename = le_util.unique_file( + csr_f, csr_filename = util.unique_file( os.path.join(path, csrname), 0o644) csr_f.write(csr_pem) csr_f.close() logger.info("Creating CSR: %s", csr_filename) - return le_util.CSR(csr_filename, csr_der, "der") + return util.CSR(csr_filename, csr_der, "der") # Lower level functions @@ -187,7 +186,7 @@ def import_csr_file(csrfile, data): :param str data: contents of the CSR file :returns: (`OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`, - le_util.CSR object representing the CSR, + util.CSR object representing the CSR, list of domains requested in the CSR) :rtype: tuple @@ -200,7 +199,7 @@ def import_csr_file(csrfile, data): logger.debug("CSR parse error (form=%s, typ=%s):", form, typ) logger.debug(traceback.format_exc()) continue - return typ, le_util.CSR(file=csrfile, data=data, form=form), domains + return typ, util.CSR(file=csrfile, data=data, form=form), domains raise errors.Error("Failed to parse CSR file: {0}".format(csrfile)) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 6752bf0c1..a8f2283fc 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -6,7 +6,7 @@ import zope.component from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.display import util as display_util logger = logging.getLogger(__name__) @@ -42,7 +42,7 @@ def get_email(more=False, invalid=False): raise errors.MissingCommandlineFlag(msg) if code == display_util.OK: - if le_util.safe_email(email): + if util.safe_email(email): return email else: # TODO catch the server's ACME invalid email address error, and @@ -119,7 +119,7 @@ def get_valid_domains(domains): valid_domains = [] for domain in domains: try: - valid_domains.append(le_util.enforce_domain_sanity(domain)) + valid_domains.append(util.enforce_domain_sanity(domain)) except errors.ConfigurationError: continue return valid_domains @@ -163,7 +163,7 @@ def _choose_names_manually(): for i, domain in enumerate(domain_list): try: - domain_list[i] = le_util.enforce_domain_sanity(domain) + domain_list[i] = util.enforce_domain_sanity(domain) except errors.ConfigurationError as e: invalid_domains[domain] = e.message diff --git a/certbot/main.py b/certbot/main.py index 4ef2e6ac8..933a102d8 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -24,7 +24,7 @@ from certbot import constants from certbot import errors from certbot import hooks from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import log from certbot import reporter from certbot import renewal @@ -229,7 +229,7 @@ def _find_duplicative_certs(config, domains): cli_config = configuration.RenewerConfiguration(config) configs_dir = cli_config.renewal_configs_dir # Verify the directory is there - le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) + util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) for renewal_file in renewal.renewal_conf_files(cli_config): try: @@ -656,12 +656,12 @@ def main(cli_args=sys.argv[1:]): # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery for directory in config.config_dir, config.work_dir: - le_util.make_or_verify_dir( + util.make_or_verify_dir( directory, constants.CONFIG_DIRS_MODE, os.geteuid(), "--strict-permissions" in cli_args) # TODO: logs might contain sensitive data such as contents of the # private key! #525 - le_util.make_or_verify_dir( + util.make_or_verify_dir( config.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') cli.possible_deprecation_warning(config) diff --git a/certbot/plugins/common.py b/certbot/plugins/common.py index 757bf19d8..007105c7b 100644 --- a/certbot/plugins/common.py +++ b/certbot/plugins/common.py @@ -12,7 +12,7 @@ from acme.jose import util as jose_util from certbot import constants from certbot import interfaces -from certbot import le_util +from certbot import util def option_namespace(name): @@ -255,7 +255,7 @@ class TLSSNI01(object): # Write out challenge cert and key with open(cert_path, "wb") as cert_chall_fd: cert_chall_fd.write(cert_pem) - with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file: + with util.safe_open(key_path, 'wb', chmod=0o400) as key_file: key_file.write(key_pem) return response diff --git a/certbot/plugins/common_test.py b/certbot/plugins/common_test.py index 0dd1cd522..f3ea714c4 100644 --- a/certbot/plugins/common_test.py +++ b/certbot/plugins/common_test.py @@ -193,7 +193,7 @@ class TLSSNI01Test(unittest.TestCase): with mock.patch("certbot.plugins.common.open", mock_open, create=True): - with mock.patch("certbot.plugins.common.le_util.safe_open", + with mock.patch("certbot.plugins.common.util.safe_open", mock_safe_open): # pylint: disable=protected-access self.assertEqual(response, self.sni._setup_challenge_cert( diff --git a/certbot/renewal.py b/certbot/renewal.py index b5b982972..d04e2d27c 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -18,7 +18,7 @@ from certbot import constants from certbot import crypto_util from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot import hooks from certbot import storage from certbot.plugins import disco as plugins_disco @@ -86,7 +86,7 @@ def _reconstitute(config, full_path): return None try: - config.domains = [le_util.enforce_domain_sanity(d) + config.domains = [util.enforce_domain_sanity(d) for d in renewal_candidate.names()] except errors.ConfigurationError as error: logger.warning("Renewal configuration file %s references a cert " diff --git a/certbot/reporter.py b/certbot/reporter.py index d509cb0b8..0c5238d12 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -11,7 +11,7 @@ from six.moves import queue # pylint: disable=import-error import zope.interface from certbot import interfaces -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) @@ -79,7 +79,7 @@ class Reporter(object): bold_on = sys.stdout.isatty() if not self.config.quiet: if bold_on: - print(le_util.ANSI_SGR_BOLD) + print(util.ANSI_SGR_BOLD) print('IMPORTANT NOTES:') first_wrapper = textwrap.TextWrapper( initial_indent=' - ', subsequent_indent=(' ' * 3)) @@ -96,7 +96,7 @@ class Reporter(object): if no_exception or msg.on_crash: if bold_on and msg.priority > self.HIGH_PRIORITY: if not self.config.quiet: - sys.stdout.write(le_util.ANSI_SGR_RESET) + sys.stdout.write(util.ANSI_SGR_RESET) bold_on = False lines = msg.text.splitlines() print(first_wrapper.fill(lines[0])) @@ -104,4 +104,4 @@ class Reporter(object): print("\n".join( next_wrapper.fill(line) for line in lines[1:])) if bold_on and not self.config.quiet: - sys.stdout.write(le_util.ANSI_SGR_RESET) + sys.stdout.write(util.ANSI_SGR_RESET) diff --git a/certbot/reverter.py b/certbot/reverter.py index 16ee5d8a4..f8140d60d 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -13,7 +13,7 @@ import zope.component from certbot import constants from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.display import util as display_util @@ -33,7 +33,7 @@ class Reverter(object): def __init__(self, config): self.config = config - le_util.make_or_verify_dir( + util.make_or_verify_dir( config.backup_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) @@ -185,7 +185,7 @@ class Reverter(object): :raises .ReverterError: if unable to add checkpoint """ - le_util.make_or_verify_dir( + util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) @@ -281,7 +281,7 @@ class Reverter(object): csvreader = csv.reader(csvfile) for command in reversed(list(csvreader)): try: - le_util.run_script(command) + util.run_script(command) except errors.SubprocessError: logger.error( "Unable to run undo command: %s", " ".join(command)) @@ -397,7 +397,7 @@ class Reverter(object): else: cp_dir = self.config.in_progress_dir - le_util.make_or_verify_dir( + util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) diff --git a/certbot/storage.py b/certbot/storage.py index 6c13eb844..b0c8245d3 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -13,12 +13,12 @@ from certbot import constants from certbot import crypto_util from certbot import errors from certbot import error_handler -from certbot import le_util +from certbot import util logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") -CURRENT_VERSION = le_util.get_strict_version(certbot.__version__) +CURRENT_VERSION = util.get_strict_version(certbot.__version__) def config_with_defaults(config=None): @@ -264,7 +264,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes conf_version = self.configuration.get("version") if (conf_version is not None and - le_util.get_strict_version(conf_version) > CURRENT_VERSION): + util.get_strict_version(conf_version) > CURRENT_VERSION): logger.warning( "Attempting to parse the version %s renewal configuration " "file found at %s with version %s of Certbot. This might not " @@ -769,7 +769,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if not os.path.exists(i): os.makedirs(i, 0o700) logger.debug("Creating directory %s.", i) - config_file, config_filename = le_util.unique_lineage_name( + config_file, config_filename = util.unique_lineage_name( cli_config.renewal_configs_dir, lineagename) if not config_filename.endswith(".conf"): raise errors.CertStorageError( diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 3facd4f7c..eccc36418 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -11,7 +11,7 @@ from acme import messages from certbot import achallenges from certbot import errors -from certbot import le_util +from certbot import util from certbot.tests import acme_util @@ -69,7 +69,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_auth.perform.side_effect = gen_auth_resp - self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) + self.mock_account = mock.Mock(key=util.Key("file_path", "PEM")) self.mock_net = mock.MagicMock(spec=acme_client.Client) self.handler = AuthHandler( diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 00c9a0a26..0beff81e7 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -22,7 +22,7 @@ from certbot import configuration from certbot import constants from certbot import crypto_util from certbot import errors -from certbot import le_util +from certbot import util from certbot import main from certbot import renewal from certbot import storage @@ -163,7 +163,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: self._call_no_clientmock(args) - os_ver = " ".join(le_util.get_os_info()) + os_ver = " ".join(util.get_os_info()) ua = acme_net.call_args[1]["user_agent"] self.assertTrue(os_ver in ua) import platform @@ -201,7 +201,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods '--key-path', 'key', '--chain-path', 'chain']) self.assertEqual(mock_pick_installer.call_count, 1) - @mock.patch('certbot.le_util.exe_exists') + @mock.patch('certbot.util.exe_exists') def test_configurator_selection(self, mock_exe_exists): mock_exe_exists.return_value = True real_plugins = disco.PluginsRegistry.find_all() @@ -983,7 +983,7 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): def tearDown(self): shutil.rmtree(self.tempdir) - @mock.patch('certbot.le_util.make_or_verify_dir') + @mock.patch('certbot.util.make_or_verify_dir') def test_find_duplicative_names(self, unused_makedir): from certbot.main import _find_duplicative_certs test_cert = test_util.load_vector('cert-san.pem') diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 8490efd9f..9156277a9 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -11,7 +11,7 @@ from acme import jose from certbot import account from certbot import errors -from certbot import le_util +from certbot import util from certbot.tests import test_util @@ -137,7 +137,7 @@ class ClientTest(unittest.TestCase): @mock.patch("certbot.client.logger") def test_obtain_certificate_from_csr(self, mock_logger): self._mock_obtain_certificate() - test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + test_csr = util.CSR(form="der", file=None, data=CSR_SAN) auth_handler = self.client.auth_handler authzr = auth_handler.get_authorizations(self.eg_domains, False) @@ -172,7 +172,7 @@ class ClientTest(unittest.TestCase): def test_obtain_certificate(self, mock_crypto_util): self._mock_obtain_certificate() - csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + csr = util.CSR(form="der", 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"] diff --git a/certbot/tests/colored_logging_test.py b/certbot/tests/colored_logging_test.py index 91c6b8c08..0a7929561 100644 --- a/certbot/tests/colored_logging_test.py +++ b/certbot/tests/colored_logging_test.py @@ -4,7 +4,7 @@ import unittest import six -from certbot import le_util +from certbot import util class StreamHandlerTest(unittest.TestCase): @@ -32,9 +32,9 @@ class StreamHandlerTest(unittest.TestCase): self.logger.debug(msg) self.assertEqual(self.stream.getvalue(), - '{0}{1}{2}\n'.format(le_util.ANSI_SGR_RED, + '{0}{1}{2}\n'.format(util.ANSI_SGR_RED, msg, - le_util.ANSI_SGR_RESET)) + util.ANSI_SGR_RESET)) if __name__ == "__main__": diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index eeea0f4ab..fa88e89e7 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -10,7 +10,7 @@ import zope.component from certbot import errors from certbot import interfaces -from certbot import le_util +from certbot import util from certbot.tests import test_util @@ -63,7 +63,7 @@ class InitSaveCSRTest(unittest.TestCase): shutil.rmtree(self.csr_dir) @mock.patch('certbot.crypto_util.make_csr') - @mock.patch('certbot.crypto_util.le_util.make_or_verify_dir') + @mock.patch('certbot.crypto_util.util.make_or_verify_dir') def test_it(self, unused_mock_verify, mock_csr): from certbot.crypto_util import init_save_csr @@ -174,9 +174,9 @@ class ImportCSRFileTest(unittest.TestCase): self.assertEqual( (OpenSSL.crypto.FILETYPE_ASN1, - le_util.CSR(file=csrfile, - data=data, - form="der"), + util.CSR(file=csrfile, + data=data, + form="der"), ["example.com"],), self._call(csrfile, data)) @@ -186,9 +186,9 @@ class ImportCSRFileTest(unittest.TestCase): self.assertEqual( (OpenSSL.crypto.FILETYPE_PEM, - le_util.CSR(file=csrfile, - data=data, - form="pem"), + util.CSR(file=csrfile, + data=data, + form="pem"), ["example.com"],), self._call(csrfile, data)) diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 05cb6b12d..874a9cc9e 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -41,13 +41,13 @@ class GetEmailTest(unittest.TestCase): def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self.assertTrue(self._call() is "foo@bar.baz") def test_ok_not_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] self.assertTrue(self._call() is "foo@bar.baz") @@ -56,7 +56,7 @@ class GetEmailTest(unittest.TestCase): invalid_txt = "There seem to be problems" base_txt = "Enter email" self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self._call() msg = self.input.call_args[0][0] diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index 85234b76a..58cc68dce 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -164,7 +164,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): errors.ReverterError, self.reverter.register_undo_command, True, ["command"]) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_run_undo_commands(self, mock_run): mock_run.side_effect = ["", errors.SubprocessError] coms = [ diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index f19b7d89d..0c88d3d55 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -682,7 +682,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertTrue(os.path.exists(os.path.join( self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem"))) - @mock.patch("certbot.storage.le_util.unique_lineage_name") + @mock.patch("certbot.storage.util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): from certbot import storage mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" diff --git a/certbot/tests/le_util_test.py b/certbot/tests/util_test.py similarity index 85% rename from certbot/tests/le_util_test.py rename to certbot/tests/util_test.py index 6e4eef0f1..ada64edb2 100644 --- a/certbot/tests/le_util_test.py +++ b/certbot/tests/util_test.py @@ -1,4 +1,4 @@ -"""Tests for certbot.le_util.""" +"""Tests for certbot.util.""" import argparse import errno import os @@ -15,13 +15,13 @@ from certbot import errors class RunScriptTest(unittest.TestCase): - """Tests for certbot.le_util.run_script.""" + """Tests for certbot.util.run_script.""" @classmethod def _call(cls, params): - from certbot.le_util import run_script + from certbot.util import run_script return run_script(params) - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_default(self, mock_popen): """These will be changed soon enough with reload.""" mock_popen().returncode = 0 @@ -31,13 +31,13 @@ class RunScriptTest(unittest.TestCase): self.assertEqual(out, "stdout") self.assertEqual(err, "stderr") - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_bad_process(self, mock_popen): mock_popen.side_effect = OSError self.assertRaises(errors.SubprocessError, self._call, ["test"]) - @mock.patch("certbot.le_util.subprocess.Popen") + @mock.patch("certbot.util.subprocess.Popen") def test_failure(self, mock_popen): mock_popen().communicate.return_value = ("", "") mock_popen().returncode = 1 @@ -46,29 +46,29 @@ class RunScriptTest(unittest.TestCase): class ExeExistsTest(unittest.TestCase): - """Tests for certbot.le_util.exe_exists.""" + """Tests for certbot.util.exe_exists.""" @classmethod def _call(cls, exe): - from certbot.le_util import exe_exists + from certbot.util import exe_exists return exe_exists(exe) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_full_path(self, mock_access, mock_isfile): mock_access.return_value = True mock_isfile.return_value = True self.assertTrue(self._call("/path/to/exe")) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_on_path(self, mock_access, mock_isfile): mock_access.return_value = True mock_isfile.return_value = True self.assertTrue(self._call("exe")) - @mock.patch("certbot.le_util.os.path.isfile") - @mock.patch("certbot.le_util.os.access") + @mock.patch("certbot.util.os.path.isfile") + @mock.patch("certbot.util.os.access") def test_not_found(self, mock_access, mock_isfile): mock_access.return_value = False mock_isfile.return_value = True @@ -76,7 +76,7 @@ class ExeExistsTest(unittest.TestCase): class MakeOrVerifyDirTest(unittest.TestCase): - """Tests for certbot.le_util.make_or_verify_dir. + """Tests for certbot.util.make_or_verify_dir. Note that it is not possible to test for a wrong directory owner, as this testing script would have to be run as root. @@ -94,7 +94,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, directory, mode): - from certbot.le_util import make_or_verify_dir + from certbot.util import make_or_verify_dir return make_or_verify_dir(directory, mode, self.uid, strict=True) def test_creates_dir_when_missing(self): @@ -117,7 +117,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): class CheckPermissionsTest(unittest.TestCase): - """Tests for certbot.le_util.check_permissions. + """Tests for certbot.util.check_permissions. Note that it is not possible to test for a wrong file owner, as this testing script would have to be run as root. @@ -132,7 +132,7 @@ class CheckPermissionsTest(unittest.TestCase): os.remove(self.path) def _call(self, mode): - from certbot.le_util import check_permissions + from certbot.util import check_permissions return check_permissions(self.path, mode, self.uid) def test_ok_mode(self): @@ -145,7 +145,7 @@ class CheckPermissionsTest(unittest.TestCase): class UniqueFileTest(unittest.TestCase): - """Tests for certbot.le_util.unique_file.""" + """Tests for certbot.util.unique_file.""" def setUp(self): self.root_path = tempfile.mkdtemp() @@ -155,7 +155,7 @@ class UniqueFileTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, mode=0o600): - from certbot.le_util import unique_file + from certbot.util import unique_file return unique_file(self.default_name, mode) def test_returns_fd_for_writing(self): @@ -190,7 +190,7 @@ class UniqueFileTest(unittest.TestCase): class UniqueLineageNameTest(unittest.TestCase): - """Tests for certbot.le_util.unique_lineage_name.""" + """Tests for certbot.util.unique_lineage_name.""" def setUp(self): self.root_path = tempfile.mkdtemp() @@ -199,7 +199,7 @@ class UniqueLineageNameTest(unittest.TestCase): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, filename, mode=0o777): - from certbot.le_util import unique_lineage_name + from certbot.util import unique_lineage_name return unique_lineage_name(self.root_path, filename, mode) def test_basic(self): @@ -214,14 +214,14 @@ class UniqueLineageNameTest(unittest.TestCase): self.assertTrue(isinstance(name, str)) self.assertTrue("wow-0009.conf" in name) - @mock.patch("certbot.le_util.os.fdopen") + @mock.patch("certbot.util.os.fdopen") def test_failure(self, mock_fdopen): err = OSError("whoops") err.errno = errno.EIO mock_fdopen.side_effect = err self.assertRaises(OSError, self._call, "wow") - @mock.patch("certbot.le_util.os.fdopen") + @mock.patch("certbot.util.os.fdopen") def test_subsequent_failure(self, mock_fdopen): self._call("wow") err = OSError("whoops") @@ -231,7 +231,7 @@ class UniqueLineageNameTest(unittest.TestCase): class SafelyRemoveTest(unittest.TestCase): - """Tests for certbot.le_util.safely_remove.""" + """Tests for certbot.util.safely_remove.""" def setUp(self): self.tmp = tempfile.mkdtemp() @@ -241,7 +241,7 @@ class SafelyRemoveTest(unittest.TestCase): shutil.rmtree(self.tmp) def _call(self): - from certbot.le_util import safely_remove + from certbot.util import safely_remove return safely_remove(self.path) def test_exists(self): @@ -255,7 +255,7 @@ class SafelyRemoveTest(unittest.TestCase): # no error, yay! self.assertFalse(os.path.exists(self.path)) - @mock.patch("certbot.le_util.os.remove") + @mock.patch("certbot.util.os.remove") def test_other_error_passthrough(self, mock_remove): mock_remove.side_effect = OSError self.assertRaises(OSError, self._call) @@ -265,7 +265,7 @@ class SafeEmailTest(unittest.TestCase): """Test safe_email.""" @classmethod def _call(cls, addr): - from certbot.le_util import safe_email + from certbot.util import safe_email return safe_email(addr) def test_valid_emails(self): @@ -293,7 +293,7 @@ class AddDeprecatedArgumentTest(unittest.TestCase): self.parser = argparse.ArgumentParser() def _call(self, argument_name, nargs): - from certbot.le_util import add_deprecated_argument + from certbot.util import add_deprecated_argument add_deprecated_argument(self.parser.add_argument, argument_name, nargs) @@ -309,14 +309,14 @@ class AddDeprecatedArgumentTest(unittest.TestCase): def _get_argparse_warnings(self, args): stderr = six.StringIO() - with mock.patch("certbot.le_util.sys.stderr", new=stderr): + with mock.patch("certbot.util.sys.stderr", new=stderr): self.parser.parse_args(args) return stderr.getvalue() def test_help(self): self._call("--old-option", 2) stdout = six.StringIO() - with mock.patch("certbot.le_util.sys.stdout", new=stdout): + with mock.patch("certbot.util.sys.stdout", new=stdout): try: self.parser.parse_args(["-h"]) except SystemExit: @@ -328,7 +328,7 @@ class EnforceDomainSanityTest(unittest.TestCase): """Test enforce_domain_sanity.""" def _call(self, domain): - from certbot.le_util import enforce_domain_sanity + from certbot.util import enforce_domain_sanity return enforce_domain_sanity(domain) def test_nonascii_str(self): @@ -341,11 +341,11 @@ class EnforceDomainSanityTest(unittest.TestCase): class GetStrictVersionTest(unittest.TestCase): - """Tests for certbot.le_util.get_strict_version.""" + """Tests for certbot.util.get_strict_version.""" @classmethod def _call(cls, *args, **kwargs): - from certbot.le_util import get_strict_version + from certbot.util import get_strict_version return get_strict_version(*args, **kwargs) def test_two_dev_versions(self): diff --git a/certbot/le_util.py b/certbot/util.py similarity index 100% rename from certbot/le_util.py rename to certbot/util.py From 5b2ab14711708099f16743801ebbac15bd31feaf Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 16:50:18 -0700 Subject: [PATCH 109/192] Revert "Pin old pkginfo version" This reverts commit 4919814dd17bdb8a7b0100ac89a5196cb4766310. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 59da23de4..4ee56576b 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,6 @@ dev_extras = [ 'nose', 'nosexcover', 'pep8', - 'pkginfo<=1.2.1', 'pylint==1.4.2', # upstream #248 'tox', 'twine', From 6598bcb53eda7e16975e6129f3700bd1944270db Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 21:53:20 -0700 Subject: [PATCH 110/192] refactor get_email --- certbot/client.py | 2 +- certbot/display/ops.py | 69 +++++++++++++++++++------------ certbot/tests/display/ops_test.py | 15 +++---- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index ba31f8760..a885d0426 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -150,7 +150,7 @@ def perform_registration(acme, config): return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error as e: if e.typ == "urn:acme:error:invalidEmail": - config.namespace.email = display_ops.get_email(more=True, invalid=True) + config.namespace.email = display_ops.get_email(invalid=True) return perform_registration(acme, config) else: raise diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 6752bf0c1..16c44a881 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -15,41 +15,56 @@ logger = logging.getLogger(__name__) z_util = zope.component.getUtility -def get_email(more=False, invalid=False): +def get_email(invalid=False, optional=True): """Prompt for valid email address. - :param bool more: explain why the email is strongly advisable, but how to - skip it - :param bool invalid: true if the user just typed something, but it wasn't - a valid-looking email + :param bool invalid: True if an invalid was provided by the user + :param bool optional: True if the user can use + --register-unsafely-without-email to avoid providing an e-mail - :returns: Email or ``None`` if cancelled by user. + :returns: e-mail address :rtype: str - """ - msg = "Enter email address (used for urgent notices and lost key recovery)" - if invalid: - msg = "There seem to be problems with that address. " + msg - if more: - msg += ('\n\nIf you really want to skip this, you can run the client with ' - '--register-unsafely-without-email but make sure you backup your ' - 'account key from /etc/letsencrypt/accounts\n\n') - try: - code, email = zope.component.getUtility(interfaces.IDisplay).input(msg) - except errors.MissingCommandlineFlag: - msg = ("You should register before running non-interactively, or provide --agree-tos" - " and --email flags") - raise errors.MissingCommandlineFlag(msg) + :raises errors.Error: if the user cancels - if code == display_util.OK: - if le_util.safe_email(email): - return email + """ + invalid_prefix = "There seem to be problems with that address. " + msg = "Enter email address (used for urgent notices and lost key recovery)" + unsafe_suggestion = ("\n\nIf you really want to skip this, you can run " + "the client with --register-unsafely-without-email " + "but make sure you backup your account key from " + "/etc/letsencrypt/accounts\n\n") + if optional: + if invalid: + msg += unsafe_suggestion else: - # TODO catch the server's ACME invalid email address error, and - # make a similar call when that happens - return get_email(more=True, invalid=(email != "")) + suggest_unsafe = True else: - return None + suggest_unsafe = False + + while True: + try: + code, email = z_util(interfaces.IDisplay).input( + invalid_prefix + msg if invalid else msg) + except errors.MissingCommandlineFlag: + msg = ("You should register before running non-interactively, " + "or provide --agree-tos and --email flags") + raise errors.MissingCommandlineFlag(msg) + + if code != display_util.OK: + if optional: + raise errors.Error( + "An e-mail address or " + "--register-unsafely-without-email must be provided.") + else: + raise errors.Error("An e-mail address must be provided.") + elif le_util.safe_email(email): + return email + elif suggest_unsafe: + msg += unsafe_suggestion + suggest_unsafe = False # add this message at most once + + invalid = bool(email) def choose_account(accounts): diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 05cb6b12d..be496a42a 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -12,6 +12,7 @@ from acme import jose from acme import messages from certbot import account +from certbot import errors from certbot import interfaces from certbot.display import util as display_util @@ -37,7 +38,7 @@ class GetEmailTest(unittest.TestCase): def test_cancel_none(self): self.input.return_value = (display_util.CANCEL, "foo@bar.baz") - self.assertTrue(self._call() is None) + self.assertRaises(errors.Error, self._call) def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") @@ -52,7 +53,7 @@ class GetEmailTest(unittest.TestCase): self.assertTrue(self._call() is "foo@bar.baz") def test_more_and_invalid_flags(self): - more_txt = "--register-unsafely-without-email" + optional_txt = "--register-unsafely-without-email" invalid_txt = "There seem to be problems" base_txt = "Enter email" self.input.return_value = (display_util.OK, "foo@bar.baz") @@ -60,16 +61,12 @@ class GetEmailTest(unittest.TestCase): mock_safe_email.return_value = True self._call() msg = self.input.call_args[0][0] - self.assertTrue(more_txt not in msg) + self.assertTrue(optional_txt not in msg) self.assertTrue(invalid_txt not in msg) self.assertTrue(base_txt in msg) - self._call(more=True) + self._call(invalid=True) msg = self.input.call_args[0][0] - self.assertTrue(more_txt in msg) - self.assertTrue(invalid_txt not in msg) - self._call(more=True, invalid=True) - msg = self.input.call_args[0][0] - self.assertTrue(more_txt in msg) + self.assertTrue(optional_txt in msg) self.assertTrue(invalid_txt in msg) self.assertTrue(base_txt in msg) From 8d802ef6fd3699e4ee1f695cdf985336606fe9fc Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 22:12:06 -0700 Subject: [PATCH 111/192] Fix up display/ops.py coverage --- certbot/tests/display/ops_test.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index be496a42a..84a4ada3f 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -39,6 +39,7 @@ class GetEmailTest(unittest.TestCase): def test_cancel_none(self): self.input.return_value = (display_util.CANCEL, "foo@bar.baz") self.assertRaises(errors.Error, self._call) + self.assertRaises(errors.Error, self._call, optional=False) def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") @@ -52,23 +53,24 @@ class GetEmailTest(unittest.TestCase): mock_safe_email.side_effect = [False, True] self.assertTrue(self._call() is "foo@bar.baz") - def test_more_and_invalid_flags(self): - optional_txt = "--register-unsafely-without-email" + def test_invalid_flag(self): invalid_txt = "There seem to be problems" - base_txt = "Enter email" self.input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self._call() - msg = self.input.call_args[0][0] - self.assertTrue(optional_txt not in msg) - self.assertTrue(invalid_txt not in msg) - self.assertTrue(base_txt in msg) + self.assertTrue(invalid_txt not in self.input.call_args[0][0]) self._call(invalid=True) - msg = self.input.call_args[0][0] - self.assertTrue(optional_txt in msg) - self.assertTrue(invalid_txt in msg) - self.assertTrue(base_txt in msg) + self.assertTrue(invalid_txt in self.input.call_args[0][0]) + + def test_optional_flag(self): + self.input.return_value = (display_util.OK, "foo@bar.baz") + with mock.patch("certbot.display.ops.le_util.safe_email") as mock_safe_email: + mock_safe_email.side_effect = [False, True] + self._call(optional=False) + for call in self.input.call_args_list: + self.assertTrue( + "--register-unsafely-without-email" not in call[0][0]) class ChooseAccountTest(unittest.TestCase): From 89279a72bd03c5f05b8b00813e7ea3801059aa4d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 22:23:36 -0700 Subject: [PATCH 112/192] Interatively ask for e-mail with register verb --- certbot/main.py | 9 ++++++--- certbot/tests/cli_test.py | 11 ++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 20b9a7ce5..c048d9277 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -391,9 +391,12 @@ def register(config, unused_plugins): if len(accounts) == 0: return "Could not find an existing account to update." if config.email is None: - return ("Currently, --update-registration can only change the e-mail " - "address\nassociated with an account. A new e-mail address is " - "required\n(hint: --email)") + if config.register_unsafely_without_email: + return ("--register-unsafely-without-email provided, however, a " + "new e-mail address must\ncurrently be provided when " + "updating a registration.") + config.namespace.email = display_ops.get_email(optional=False) + acc, acme = _determine_account(config) acme_client = client.Client(config, acc, None, None, acme=acme) # We rely on an exception to interrupt this process if it didn't work. diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index b348e8ea7..b6103c2e8 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -942,16 +942,17 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods "user@example.org"]) self.assertTrue("Could not find an existing account" in x[0]) - def test_update_registration_no_email(self): + def test_update_registration_unsafely(self): # This test will become obsolete when register --update-registration - # supports updating something other than the e-mail address! - # with mock.patch('certbot.main.client') as mocked_client: + # supports removing an e-mail address from the account with mock.patch('certbot.main.account') as mocked_account: mocked_storage = mock.MagicMock() mocked_account.AccountFileStorage.return_value = mocked_storage mocked_storage.find_all.return_value = ["an account"] - x = self._call_no_clientmock(["register", "--update-registration"]) - self.assertTrue("can only change the e-mail" in x[0]) + x = self._call_no_clientmock( + "register --update-registration " + "--register-unsafely-without-email".split()) + self.assertTrue("--register-unsafely-without-email" in x[0]) def test_update_registration_with_email(self): with mock.patch('certbot.main.client') as mocked_client: From 14b45b795a0a37af9b039a10728d290b6a113874 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 25 May 2016 22:45:57 -0700 Subject: [PATCH 113/192] give register full test coverage --- certbot/tests/cli_test.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index b6103c2e8..0bf34eb10 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -954,7 +954,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods "--register-unsafely-without-email".split()) self.assertTrue("--register-unsafely-without-email" in x[0]) - def test_update_registration_with_email(self): + @mock.patch('certbot.main.display_ops.get_email') + @mock.patch('certbot.main.zope.component.getUtility') + def test_update_registration_with_email(self, mock_utility, mock_email): + email = "user@example.com" + mock_email.return_value = email with mock.patch('certbot.main.client') as mocked_client: with mock.patch('certbot.main.account') as mocked_account: with mock.patch('certbot.main._determine_account') as mocked_det: @@ -966,8 +970,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods acme_client = mock.MagicMock() mocked_client.Client.return_value = acme_client x = self._call_no_clientmock( - ["register", "--update-registration", "--email", - "user@example.org"]) + ["register", "--update-registration"]) # When registration change succeeds, the return value # of register() is None self.assertTrue(x[0] is None) @@ -977,6 +980,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods acme_client.acme.update_registration.called) # and we saved the updated registration on disk self.assertTrue(mocked_storage.save_regr.called) + self.assertTrue( + email in mock_utility().add_message.call_args[0][0]) def test_conflicting_args(self): args = ['renew', '--dialog', '--text'] From 1322ae12ce2bcd362399023ee2852927f7910a1b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 26 May 2016 10:20:47 -0700 Subject: [PATCH 114/192] Stop packaging letshelp --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index abaad09ff..ca09c03a4 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -45,7 +45,7 @@ export GPG_TTY=$(tty) PORT=${PORT:-1234} # subpackages to be released -SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letshelp-certbot letsencrypt letsencrypt-apache letsencrypt-nginx letshelp-letsencrypt"} +SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letsencrypt letsencrypt-apache letsencrypt-nginx"} subpkgs_modules="$(echo $SUBPKGS | sed s/-/_/g)" # certbot_compatibility_test is not packaged because: # - it is not meant to be used by anyone else than Certbot devs From 7e039d15044738bca2326f26e8e7bdc2b88386db Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 26 May 2016 10:24:57 -0700 Subject: [PATCH 115/192] With us packaging the shim packages, there are more lines in letsencrypt-auto-requirements.txt that will change with every release. This change strips the hashes of the previous packages before adding the new ones. --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index ca09c03a4..154172322 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -176,7 +176,7 @@ if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*15 " ; then fi # perform hideous surgery on requirements.txt... -head -n -9 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ +head -n -15 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ cat /tmp/hashes.$$ >> /tmp/req.$$ cp /tmp/req.$$ letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt From a7edc4b1e5ac9f99875ca7bc887d6db4b3415c97 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 26 May 2016 10:33:18 -0700 Subject: [PATCH 116/192] Previously, the script relied on global `pip` for hashing packages. This doesn't work if you don't have `pip` installed (like me) and I think using `pip` from the venv should be preferred to ensure you are using the latest `pip` (which was updated in the venv earlier in the script). --- tools/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release.sh b/tools/release.sh index 154172322..89a2f5140 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -162,13 +162,13 @@ for module in certbot $subpkgs_modules ; do echo testing $module nosetests $module done -deactivate # pin pip hashes of the things we just built for pkg in acme certbot certbot-apache letsencrypt letsencrypt-apache ; do echo $pkg==$version \\ pip hash dist."$version/$pkg"/*.{whl,gz} | grep "^--hash" | python2 -c 'from sys import stdin; input = stdin.read(); print " ", input.replace("\n--hash", " \\\n --hash"),' done > /tmp/hashes.$$ +deactivate if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*15 " ; then echo Unexpected pip hash output From f0dc0de40a50c6b6957691a8c43fc51598f05a04 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 26 May 2016 13:43:00 -0500 Subject: [PATCH 117/192] Catch more le_util usage in certbot-apache --- .../certbot_apache/tests/configurator_test.py | 48 +++++++++---------- .../certbot_apache/tests/constants_test.py | 6 +-- .../certbot_apache/tests/tls_sni_01_test.py | 4 +- certbot-apache/certbot_apache/tests/util.py | 4 +- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index d7a5fd75a..a2e39de47 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -49,14 +49,14 @@ class MultipleVhostsTest(util.ApacheTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_no_install(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( errors.NoInstallationError, self.config.prepare) @mock.patch("certbot_apache.parser.ApacheParser") - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_version(self, mock_exe_exists, _): mock_exe_exists.return_value = True self.config.version = None @@ -67,7 +67,7 @@ class MultipleVhostsTest(util.ApacheTest): errors.NotSupportedError, self.config.prepare) @mock.patch("certbot_apache.parser.ApacheParser") - @mock.patch("certbot_apache.configurator.le_util.exe_exists") + @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_old_aug(self, mock_exe_exists, _): mock_exe_exists.return_value = True self.config.config_test = mock.Mock() @@ -268,8 +268,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.is_site_enabled, "irrelevant") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") @mock.patch("certbot_apache.parser.subprocess.Popen") def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script): mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "") @@ -287,7 +287,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.NotSupportedError, self.config.enable_mod, "ssl") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_enable_mod_no_disable(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( @@ -695,7 +695,7 @@ class MultipleVhostsTest(util.ApacheTest): self.config.cleanup([achall1, achall2]) self.assertTrue(mock_restart.called) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_get_version(self, mock_script): mock_script.return_value = ( "Server Version: Apache/2.4.2 (Debian)", "") @@ -717,21 +717,21 @@ class MultipleVhostsTest(util.ApacheTest): mock_script.side_effect = errors.SubprocessError("Can't find program") self.assertRaises(errors.PluginError, self.config.get_version) - @mock.patch("certbot_apache.configurator.le_util.run_script") + @mock.patch("certbot_apache.configurator.util.run_script") def test_restart(self, _): self.config.restart() - @mock.patch("certbot_apache.configurator.le_util.run_script") + @mock.patch("certbot_apache.configurator.util.run_script") def test_restart_bad_process(self, mock_run_script): mock_run_script.side_effect = [None, errors.SubprocessError] self.assertRaises(errors.MisconfigurationError, self.config.restart) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test(self, _): self.config.config_test() - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test_bad_process(self, mock_run_script): mock_run_script.side_effect = errors.SubprocessError @@ -771,7 +771,7 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot_apache.configurator.ApacheConfigurator._get_http_vhost") @mock.patch("certbot_apache.display_ops.select_vhost") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_enhance_unknown_vhost(self, mock_exe, mock_sel_vhost, mock_get): self.config.parser.modules.add("rewrite_module") mock_exe.return_value = True @@ -792,8 +792,8 @@ class MultipleVhostsTest(util.ApacheTest): errors.PluginError, self.config.enhance, "certbot.demo", "unknown_enhancement") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_ocsp_stapling(self, mock_exe, mock_run_script): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -821,7 +821,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(len(stapling_cache_aug_path), 1) - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_ocsp_stapling_twice(self, mock_exe): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -848,7 +848,7 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(len(stapling_cache_aug_path), 1) - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.exe_exists") def test_ocsp_unsupported_apache_version(self, mock_exe): mock_exe.return_value = True self.config.parser.update_runtime_variables = mock.Mock() @@ -871,8 +871,8 @@ class MultipleVhostsTest(util.ApacheTest): http_vh = self.config._get_http_vhost(ssl_vh) self.assertTrue(http_vh.ssl == False) - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_http_header_hsts(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -909,8 +909,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.enhance, "encryption-example.demo", "ensure-http-header", "Strict-Transport-Security") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_http_header_uir(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() self.config.parser.modules.add("mod_ssl.c") @@ -947,8 +947,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.enhance, "encryption-example.demo", "ensure-http-header", "Upgrade-Insecure-Requests") - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_redirect_well_formed_http(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True @@ -991,8 +991,8 @@ class MultipleVhostsTest(util.ApacheTest): # pylint: disable=protected-access self.assertTrue(self.config._is_rewrite_engine_on(self.vh_truth[3])) - @mock.patch("certbot.le_util.run_script") - @mock.patch("certbot.le_util.exe_exists") + @mock.patch("certbot.util.run_script") + @mock.patch("certbot.util.exe_exists") def test_redirect_with_existing_rewrite(self, mock_exe, _): self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True diff --git a/certbot-apache/certbot_apache/tests/constants_test.py b/certbot-apache/certbot_apache/tests/constants_test.py index d970c96be..c040030df 100644 --- a/certbot-apache/certbot_apache/tests/constants_test.py +++ b/certbot-apache/certbot_apache/tests/constants_test.py @@ -8,19 +8,19 @@ from certbot_apache import constants class ConstantsTest(unittest.TestCase): - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_debian_value(self, os_info): os_info.return_value = ('Debian', '', '') self.assertEqual(constants.os_constant("vhost_root"), "/etc/apache2/sites-available") - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_centos_value(self, os_info): os_info.return_value = ('CentOS Linux', '', '') self.assertEqual(constants.os_constant("vhost_root"), "/etc/httpd/conf.d") - @mock.patch("certbot.le_util.get_os_info") + @mock.patch("certbot.util.get_os_info") def test_get_default_value(self, os_info): os_info.return_value = ('Nonexistent Linux', '', '') self.assertEqual(constants.os_constant("vhost_root"), diff --git a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py index aa6a2a09c..5e369e3db 100644 --- a/certbot-apache/certbot_apache/tests/tls_sni_01_test.py +++ b/certbot-apache/certbot_apache/tests/tls_sni_01_test.py @@ -38,8 +38,8 @@ class TlsSniPerformTest(util.ApacheTest): resp = self.sni.perform() self.assertEqual(len(resp), 0) - @mock.patch("certbot.le_util.exe_exists") - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.exe_exists") + @mock.patch("certbot.util.run_script") def test_perform1(self, _, mock_exists): mock_register = mock.Mock() self.sni.configurator.reverter.register_undo_command = mock_register diff --git a/certbot-apache/certbot_apache/tests/util.py b/certbot-apache/certbot_apache/tests/util.py index 8935ee908..050876687 100644 --- a/certbot-apache/certbot_apache/tests/util.py +++ b/certbot-apache/certbot_apache/tests/util.py @@ -95,8 +95,8 @@ def get_apache_configurator( in_progress_dir=os.path.join(backups, "IN_PROGRESS"), work_dir=work_dir) - with mock.patch("certbot_apache.configurator.le_util.run_script"): - with mock.patch("certbot_apache.configurator.le_util." + with mock.patch("certbot_apache.configurator.util.run_script"): + with mock.patch("certbot_apache.configurator.util." "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True with mock.patch("certbot_apache.parser.ApacheParser." From 2d121585c92a1f37eb51573984d5676228882df7 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 26 May 2016 15:51:56 -0500 Subject: [PATCH 118/192] Catch more le_util in certbot-nginx --- certbot-nginx/certbot_nginx/tests/configurator_test.py | 8 ++++---- certbot-nginx/certbot_nginx/tests/util.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index b36802939..30f287249 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -30,7 +30,7 @@ class NginxConfiguratorTest(util.NginxTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) - @mock.patch("certbot_nginx.configurator.le_util.exe_exists") + @mock.patch("certbot_nginx.configurator.util.exe_exists") def test_prepare_no_install(self, mock_exe_exists): mock_exe_exists.return_value = False self.assertRaises( @@ -40,7 +40,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEquals((1, 6, 2), self.config.version) self.assertEquals(5, len(self.config.parser.parsed)) - @mock.patch("certbot_nginx.configurator.le_util.exe_exists") + @mock.patch("certbot_nginx.configurator.util.exe_exists") @mock.patch("certbot_nginx.configurator.subprocess.Popen") def test_prepare_initializes_version(self, mock_popen, mock_exe_exists): mock_popen().communicate.return_value = ( @@ -362,11 +362,11 @@ class NginxConfiguratorTest(util.NginxTest): mock_popen.side_effect = OSError("Can't find program") self.assertRaises(errors.MisconfigurationError, self.config.restart) - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test(self, _): self.config.config_test() - @mock.patch("certbot.le_util.run_script") + @mock.patch("certbot.util.run_script") def test_config_test_bad_process(self, mock_run_script): mock_run_script.side_effect = errors.SubprocessError self.assertRaises(errors.MisconfigurationError, self.config.config_test) diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 3c4731700..ddacd041b 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -51,7 +51,7 @@ def get_nginx_configurator( with mock.patch("certbot_nginx.configurator.NginxConfigurator." "config_test"): - with mock.patch("certbot_nginx.configurator.le_util." + with mock.patch("certbot_nginx.configurator.util." "exe_exists") as mock_exe_exists: mock_exe_exists.return_value = True config = configurator.NginxConfigurator( From b05258243a2b367632c207f3d2a14cc924258225 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 26 May 2016 15:57:50 -0500 Subject: [PATCH 119/192] More le_util in docs and compatibility tests --- .../configurators/apache/common.py | 4 ++-- docs/api/le_util.rst | 5 ----- docs/api/util.rst | 5 +++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 docs/api/le_util.rst create mode 100644 docs/api/util.rst diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index f57e0512d..9148666fc 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -47,10 +47,10 @@ class Proxy(configurators_common.Proxy): "certbot_apache.parser.subprocess", mock_subprocess).start() mock.patch( - "certbot.le_util.subprocess", + "certbot.util.subprocess", mock_subprocess).start() mock.patch( - "certbot_apache.configurator.le_util.exe_exists", + "certbot_apache.configurator.util.exe_exists", _is_apache_command).start() patch = mock.patch( diff --git a/docs/api/le_util.rst b/docs/api/le_util.rst deleted file mode 100644 index c9e332745..000000000 --- a/docs/api/le_util.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`certbot.le_util` --------------------------- - -.. automodule:: certbot.le_util - :members: diff --git a/docs/api/util.rst b/docs/api/util.rst new file mode 100644 index 000000000..7d0e33501 --- /dev/null +++ b/docs/api/util.rst @@ -0,0 +1,5 @@ +:mod:`certbot.util` +-------------------------- + +.. automodule:: certbot.util + :members: From 46d8f6e18c1d053a5811f55a619a31a75d21fc89 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 27 May 2016 13:30:46 -0700 Subject: [PATCH 120/192] Release 0.7.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 99 ++++++++++-------- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 32 ++++-- letsencrypt-apache/setup.py | 2 +- letsencrypt-auto | 99 ++++++++++-------- letsencrypt-auto-source/certbot-auto.asc | 14 +-- letsencrypt-auto-source/letsencrypt-auto | 32 +++--- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 30 +++--- letsencrypt-nginx/setup.py | 2 +- letsencrypt/setup.py | 2 +- 15 files changed, 176 insertions(+), 146 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index d38864dc1..3b01a6b73 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0.dev0' +version = '0.7.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 56c6a451d..8974df882 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.7.0.dev0' +version = '0.7.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 8c6e6c486..5fbef43b1 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.6.0" +LE_AUTO_VERSION="0.7.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -38,17 +38,6 @@ Help for certbot itself cannot be provided until it is installed. All arguments are accepted and forwarded to the Certbot client when run." -while getopts ":hnv" arg; do - case $arg in - h) - HELP=1;; - n) - ASSUME_YES=1;; - v) - VERBOSE=1;; - esac -done - for arg in "$@" ; do case "$arg" in --debug) @@ -65,9 +54,26 @@ for arg in "$@" ; do ASSUME_YES=1;; --verbose) VERBOSE=1;; + -[!-]*) + while getopts ":hnv" short_arg $arg; do + case "$short_arg" in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac + done;; esac done +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -107,12 +113,6 @@ else SUDO= fi -if [ $BASENAME = "letsencrypt-auto" ]; then - # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 - HELP=0 -fi - ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -425,7 +425,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." @@ -435,7 +436,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on OS X if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then echo "Applying augeas workaround" - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + $SUDO mkdir -p /usr/local/lib/ + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then @@ -451,6 +453,11 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + # Install required OS packages: Bootstrap() { @@ -483,8 +490,10 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + 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 + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo @@ -523,6 +532,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -706,21 +716,21 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.7.0 \ + --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ + --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 +certbot==0.7.0 \ + --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ + --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 +certbot-apache==0.7.0 \ + --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ + --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 +letsencrypt-apache==0.7.0 \ + --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ + --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -880,7 +890,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" @@ -890,14 +899,16 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run certbot..." + if [ -n "$SUDO" ]; then + # SUDO is su wrapper or sudo + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else # sudo - echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" fi @@ -923,8 +934,8 @@ else fi if [ "$NO_SELF_UPGRADE" != 1 ]; then - echo "Checking for new version..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: @@ -997,7 +1008,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most @@ -1016,7 +1027,7 @@ def verified_new_le_auto(get, tag, temp_dir): """ le_auto_dir = environ.get( 'LE_AUTO_DIR_TEMPLATE', - 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'https://raw.githubusercontent.com/certbot/certbot/%s/' 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') @@ -1079,8 +1090,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8f9452c5a..b1196eb23 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.7.0.dev0' +version = '0.7.0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index a74b93093..bfdfd5f66 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.7.0.dev0' +version = '0.7.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 85f370e7a..06a1930db 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.7.0.dev0' +__version__ = '0.7.0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index cb4bace58..4026f1cc8 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -28,6 +28,7 @@ optional arguments: require additional command line flags; the client will try to explain which ones are required if it finds one missing (default: False) + --dialog Run using dialog (default: False) --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' @@ -130,6 +131,10 @@ security: Security parameters & server settings --rsa-key-size N Size of the RSA key. (default: 2048) + --must-staple Adds the OCSP Must Staple extension to the + certificate. Autoconfigures OCSP Stapling for + supported setups (Apache version >= 2.3.3 ). (default: + False) --redirect Automatically redirect all HTTP traffic to HTTPS for the newly authenticated vhost. (default: None) --no-redirect Do not automatically redirect all HTTP traffic to @@ -148,6 +153,11 @@ security: --no-uir Do not automatically set the "Content-Security-Policy: upgrade-insecure-requests" header to every HTTP response. (default: None) + --staple-ocsp Enables OCSP Stapling. A valid OCSP response is + stapled to the certificate that the server offers + during TLS. (default: None) + --no-staple-ocsp Do not automatically enable OCSP Stapling. (default: + None) --strict-permissions Require that all configuration files are owned by the current user; only needed if your config is somewhere unsafe like /tmp/ (default: False) @@ -173,7 +183,9 @@ renew: Command to be run in a shell after attempting to obtain/renew certificates. Can be used to deploy renewed certificates, or to restart any servers that - were stopped by --pre-hook. (default: None) + were stopped by --pre-hook. This is only run if an + attempt was made to obtain/renew a certificate. + (default: None) --renew-hook RENEW_HOOK Command to be run in a shell once for each successfully renewed certificate.For this command, the @@ -263,15 +275,6 @@ plugins: --webroot Obtain certs by placing files in a webroot directory. (default: False) -nginx: - Nginx Web Server - currently doesn't work - - --nginx-server-root NGINX_SERVER_ROOT - Nginx server root directory. (default: /etc/nginx) - --nginx-ctl NGINX_CTL - Path to the 'nginx' binary, used for 'configtest' and - retrieving nginx version number. (default: nginx) - standalone: Automatically use a temporary webserver @@ -288,6 +291,15 @@ manual: Automatically allows public IP logging. (default: False) +nginx: + Nginx Web Server - currently doesn't work + + --nginx-server-root NGINX_SERVER_ROOT + Nginx server root directory. (default: /etc/nginx) + --nginx-ctl NGINX_CTL + Path to the 'nginx' binary, used for 'configtest' and + retrieving nginx version number. (default: nginx) + webroot: Place files in webroot directory diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index b94746150..29b3df09f 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0.dev0' +version = '0.7.0' # This package is a simple shim around certbot-apache diff --git a/letsencrypt-auto b/letsencrypt-auto index 8c6e6c486..5fbef43b1 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.6.0" +LE_AUTO_VERSION="0.7.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -38,17 +38,6 @@ Help for certbot itself cannot be provided until it is installed. All arguments are accepted and forwarded to the Certbot client when run." -while getopts ":hnv" arg; do - case $arg in - h) - HELP=1;; - n) - ASSUME_YES=1;; - v) - VERBOSE=1;; - esac -done - for arg in "$@" ; do case "$arg" in --debug) @@ -65,9 +54,26 @@ for arg in "$@" ; do ASSUME_YES=1;; --verbose) VERBOSE=1;; + -[!-]*) + while getopts ":hnv" short_arg $arg; do + case "$short_arg" in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac + done;; esac done +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + # certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but @@ -107,12 +113,6 @@ else SUDO= fi -if [ $BASENAME = "letsencrypt-auto" ]; then - # letsencrypt-auto does not respect --help or --yes for backwards compatibility - ASSUME_YES=1 - HELP=0 -fi - ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -425,7 +425,8 @@ BootstrapMac() { $pkgcmd augeas $pkgcmd dialog - if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then + if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \ + -o "$(which python)" = "/usr/bin/python" ]; then # We want to avoid using the system Python because it requires root to use pip. # python.org, MacPorts or HomeBrew Python installations should all be OK. echo "Installing python..." @@ -435,7 +436,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on OS X if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then echo "Applying augeas workaround" - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib + $SUDO mkdir -p /usr/local/lib/ + $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then @@ -451,6 +453,11 @@ BootstrapMac() { fi } +BootstrapSmartOS() { + pkgin update + pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' +} + # Install required OS packages: Bootstrap() { @@ -483,8 +490,10 @@ Bootstrap() { ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" BootstrapMac - elif grep -iq "Amazon Linux" /etc/issue ; then + 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 + ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS else echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo @@ -523,6 +532,7 @@ if [ "$1" = "--le-auto-phase2" ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" @@ -706,21 +716,21 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.7.0 \ + --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ + --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 +certbot==0.7.0 \ + --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ + --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 +certbot-apache==0.7.0 \ + --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ + --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 +letsencrypt-apache==0.7.0 \ + --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ + --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -880,7 +890,6 @@ UNLIKELY_EOF PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` PIP_STATUS=$? set -e - rm -rf "$TEMP_DIR" if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) echo "Had a problem while installing Python packages:" @@ -890,14 +899,16 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run certbot..." + if [ -n "$SUDO" ]; then + # SUDO is su wrapper or sudo + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" $SUDO "$VENV_BIN/letsencrypt" "$@" else # sudo - echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" fi @@ -923,8 +934,8 @@ else fi if [ "$NO_SELF_UPGRADE" != 1 ]; then - echo "Checking for new version..." TEMP_DIR=$(TempDir) + trap 'rm -rf "$TEMP_DIR"' EXIT # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: @@ -997,7 +1008,7 @@ def latest_stable_version(get): """Return the latest stable release of letsencrypt.""" metadata = loads(get( environ.get('LE_AUTO_JSON_URL', - 'https://pypi.python.org/pypi/letsencrypt/json'))) + 'https://pypi.python.org/pypi/certbot/json'))) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. # The regex is a sufficient regex for picking out prereleases for most @@ -1016,7 +1027,7 @@ def verified_new_le_auto(get, tag, temp_dir): """ le_auto_dir = environ.get( 'LE_AUTO_DIR_TEMPLATE', - 'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/' + 'https://raw.githubusercontent.com/certbot/certbot/%s/' 'letsencrypt-auto-source/') % tag write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') @@ -1079,8 +1090,6 @@ UNLIKELY_EOF # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf "$TEMP_DIR" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 8b4f34c70..454cbe598 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXM9ZDAAoJEE0XyZXNl3XyzGkH/2KeR0jYxXKlvwfCkxU6hSC0 -eXcxZVQk59hCSvkNGE6Mj6rwQcyjSqmRp14MaJpq7NZADN6F+HWb6VB/Wq6moMQs -PJtthqwhF767Qg+Py9Hp6XmlKscjXB6AKCVxq5TBwEIOTtj0rhQRLF9/+GW6jFuf -kT6aUcDWNjOyWWUtp9vOVprDtegrltp0/2DNitlvPu263pKC+7I3GyLTq4fKP4EE -auZSAhFry9SNR3Usf2wD3kzhvLSrT3h9Yh5oA04oaX9H6e86EHwt6RJJRHpg8s6b -e0CBIIuaRJEmdiMUWlV/gAfH6M2PbG1wtJdxc0ThNEoWAjTsopr61BoHJ3cpCy4= -=+e7/ +iQEcBAABAgAGBQJXSK5DAAoJEE0XyZXNl3Xyyb4H/Ahy9/8ADDaN5V/O/6kl6gE5 +amQfm8T10EUD8APnNWYrYKBYruDBVvH0KiEcuAEs7q4xE5BaQatlobSnsHfv4AWW +TwInk2lRxYZ++MwwQf3DrqMK5QKfcoVnViZsRpZ8gHMLzsJllRm7R5eaTewO2ViM +KM+yDB3UsquLUvE4d3/hgBl2mXAUwsxLeFreZayvpoTcX2ARnzbtKqMaIBYDYWcx +DewWtDsPrhKFpb2DY06S6JLmEttysUgv+hbKlaVO0yZ8cCUehkzBIGYoeS4chOLq +fonNCzB8u3RtnLEFiPIy0N+A592jbLsqqUkxjammaJq3lH7nitduMLnpvGKt4yc= +=ex1J -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index fee63c2f5..5fbef43b1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0.dev0" +LE_AUTO_VERSION="0.7.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -716,21 +716,21 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.7.0 \ + --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ + --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 +certbot==0.7.0 \ + --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ + --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 +certbot-apache==0.7.0 \ + --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ + --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 +letsencrypt-apache==0.7.0 \ + --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ + --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index cb360f89ef5443af2c6bc157704a98d3cd68fcef..e7e6546a25178083f007351bbba85fda56c87bb2 100644 GIT binary patch literal 256 zcmV+b0ssD(3r1$c09~FFRL*(rn=D5xr$W>4tf{yJ0Yd>ifFOs3`#iS_w(uP+p(fG& zTEmPXK+5%ZpiHyaPtmN4i1xvJn%fgm9b2nF4!4kuT8ov;Uq`~S8bQzaVDw}C5Xzw8cyZq9H=SydJxxr0AB GYGOMK)`FS< diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index f4ceae536..6405efd78 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -178,18 +178,18 @@ mock==1.0.1 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.6.0 \ - --hash=sha256:cbe4e7a340a19725a8740ed86e30abdbe18fc22c4c6022b7a8e56642d502bcc3 \ - --hash=sha256:ec4e6009dfbd629b58473eb06bbebfd9fb2a79fc8831c149e9205bc38a98ecc6 -certbot==0.6.0 \ - --hash=sha256:a893632d228864b0a751db9f3fdd93439ed34b988ea21b64fb0f0fa2ceded6a2 \ - --hash=sha256:80b0b7dc5afeec2816ef638a61e7c628d73cd72666eebf4984be426d1c2b492d -certbot-apache==0.6.0 \ - --hash=sha256:0ab077f0913b81ed5c1b141c3a7c4c0228ef3738d8d61a93db794d9a80718d43 \ - --hash=sha256:1cfbe751209079a803758f472200816fac559f2a36fdd582d25e3ba5601423a1 -letsencrypt==0.6.0 \ - --hash=sha256:93196c7dcd57272a753e525d145c5a9987c8968c22ec954bcf83dcc9d2499a76 \ - --hash=sha256:a16d6c395f1bf5fd61a28ef83dc78f42dbecbad9d00be6236f2ad8915645c154 -letsencrypt-apache==0.6.0 \ - --hash=sha256:02fadc52a0796e53978c508beec9c53e1fc047660240832b9bde5d53ab3a1379 \ - --hash=sha256:1c5522d94d7750bdb9bfa6201d2c263e914f662c9d0079e673167233cf4364f1 +acme==0.7.0 \ + --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ + --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 +certbot==0.7.0 \ + --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ + --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 +certbot-apache==0.7.0 \ + --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ + --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 +letsencrypt-apache==0.7.0 \ + --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ + --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 95ffd6cd8..3acfa9bb8 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0.dev0' +version = '0.7.0' # This package is a simple shim around certbot-nginx diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py index 7c974ea9b..fd6544265 100644 --- a/letsencrypt/setup.py +++ b/letsencrypt/setup.py @@ -20,7 +20,7 @@ readme = read_file(os.path.join(here, 'README.rst')) install_requires = ['certbot'] -version = '0.7.0.dev0' +version = '0.7.0' setup( From 7153220b4106031fcf7cc4a958d0a3f44215d6f8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 27 May 2016 13:30:54 -0700 Subject: [PATCH 121/192] Bump version to 0.8.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-apache/setup.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-nginx/setup.py | 2 +- letsencrypt/setup.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 3b01a6b73..c25cb5c00 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.7.0' +version = '0.8.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 8974df882..2a4716db7 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.7.0' +version = '0.8.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 b1196eb23..8d2bd925d 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.7.0' +version = '0.8.0.dev0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index bfdfd5f66..bb8b3414e 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.7.0' +version = '0.8.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 06a1930db..dc0e2764d 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.7.0' +__version__ = '0.8.0.dev0' diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 29b3df09f..09703841c 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0' +version = '0.8.0.dev0' # This package is a simple shim around certbot-apache diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 5fbef43b1..2de0652a9 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0" +LE_AUTO_VERSION="0.8.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 3acfa9bb8..25db12a47 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -16,7 +16,7 @@ here = os.path.abspath(os.path.dirname(__file__)) readme = read_file(os.path.join(here, 'README.rst')) -version = '0.7.0' +version = '0.8.0.dev0' # This package is a simple shim around certbot-nginx diff --git a/letsencrypt/setup.py b/letsencrypt/setup.py index fd6544265..4541e85fe 100644 --- a/letsencrypt/setup.py +++ b/letsencrypt/setup.py @@ -20,7 +20,7 @@ readme = read_file(os.path.join(here, 'README.rst')) install_requires = ['certbot'] -version = '0.7.0' +version = '0.8.0.dev0' setup( From b627f643772444b965f0e03b0da19f99b5d7c4f5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 27 May 2016 15:04:59 -0700 Subject: [PATCH 122/192] Fix stray merge error --- certbot/tests/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 09a882c89..671da16f0 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -177,7 +177,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods import platform plat = platform.platform() if "linux" in plat.lower(): - self.assertTrue(le_util.get_os_info_ua() in ua) + self.assertTrue(util.get_os_info_ua() in ua) with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net: ua = "bandersnatch" From 24915f6d008e50c2a242e9ac7e752ad28cb0f623 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 27 May 2016 16:45:12 -0700 Subject: [PATCH 123/192] Lint --- certbot/tests/util_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 4cbc9b663..8e1b330ed 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -10,7 +10,6 @@ import unittest import mock import six -import certbot from certbot import errors from certbot.tests import test_util From b24768751753c6ad2aead5167b5ca3eaf345b155 Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Sat, 28 May 2016 10:45:57 +0100 Subject: [PATCH 124/192] Don't call os.pid() the default kwargs to atexit_print_messages Whilst it has no side-effects, this results in non-reproducible output when generating the certbot documentation: http://i.imgur.com/Q6VGi9S.jpg Signed-off-by: Chris Lamb --- certbot/reporter.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/certbot/reporter.py b/certbot/reporter.py index d509cb0b8..fe34ece54 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -16,6 +16,11 @@ from certbot import le_util logger = logging.getLogger(__name__) +# Store the pid of the process that first imported this module so that +# atexit_print_messages side-effects such as error reporting can be limited to +# this process and not any fork()'d children. +INITIAL_PID = os.getpid() + @zope.interface.implementer(interfaces.IReporter) class Reporter(object): @@ -55,12 +60,14 @@ class Reporter(object): self.messages.put(self._msg_type(priority, msg, on_crash)) logger.info("Reporting to user: %s", msg) - def atexit_print_messages(self, pid=os.getpid()): + def atexit_print_messages(self, pid=None): """Function to be registered with atexit to print messages. :param int pid: Process ID """ + if pid is None: + pid = INITIAL_PID # This ensures that messages are only printed from the process that # created the Reporter. if pid == os.getpid(): From 2e12fda802627b91953e0bc5397a7aa7fe4cc544 Mon Sep 17 00:00:00 2001 From: Shaun Cummiskey Date: Sat, 28 May 2016 16:08:28 -0500 Subject: [PATCH 125/192] Spelling correction --- certbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index 933a102d8..96c0221a2 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -292,7 +292,7 @@ def _report_new_cert(config, cert_path, fullchain_path): msg = ('Congratulations! Your certificate {0} been saved at {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 ceriticates, run "{3} renew"' + 'To non-interactively renew *all* of your certificates, run "{3} renew"' .format(and_chain, path, expiry, cli.cli_command, verbswitch)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) From 2a9e190cf22501236f6a0fbc5978367278dbc9b8 Mon Sep 17 00:00:00 2001 From: LeCoyote Date: Sun, 29 May 2016 15:54:01 +0400 Subject: [PATCH 126/192] Changed Gentoo os_info string See bug #3091 --- certbot-apache/certbot_apache/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index ffbedc90b..f73004a81 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -80,7 +80,7 @@ CLI_DEFAULTS = { "red hat enterprise linux server": CLI_DEFAULTS_CENTOS, "rhel": CLI_DEFAULTS_CENTOS, "amazon": CLI_DEFAULTS_CENTOS, - "gentoo base system": CLI_DEFAULTS_GENTOO, + "gentoo": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, } """CLI defaults.""" From d21f8fa657cf4fbea47da47f1dc0bb5e1c9e594a Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 20 May 2016 08:50:00 +0300 Subject: [PATCH 127/192] Add --disable-hook-validation As discussed in #3020. --- certbot/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index bcb7785c5..f3decec05 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -356,7 +356,8 @@ class HelpfulArgumentParser(object): " {0} conflicts with dialog_mode").format(arg) ) - hooks.validate_hooks(parsed_args) + if parsed_args.validate_hooks: + hooks.validate_hooks(parsed_args) return parsed_args @@ -792,6 +793,14 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "For this command, the shell variable $RENEWED_LINEAGE will point to the" "config live subdirectory containing the new certs and keys; the shell variable " "$RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains") + helpful.add( + "renew", "--disable-hook-validation", + action='store_false', dest='validate_hooks', default=True, + help="Ordinarily the commands specified for --pre-hook/--post-hook/--renew-hook" + " will be checked for validity, to see if the programs being run are in the $PATH," + " so that mistakes can be caught early, even when the hooks aren't being run just yet." + " The validation is rather simplistic and fails if you use more advanced" + " shell constructs, so you can use this switch to disable it.") helpful.add_deprecated_argument("--agree-dev-preview", 0) From b324845a9cc76c8234005f700763b7951945bb01 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 31 May 2016 15:24:19 -0700 Subject: [PATCH 128/192] fixes #1080 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4ee56576b..53fde5282 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ install_requires = [ 'configobj', 'cryptography>=0.7', # load_pem_x509_certificate 'parsedatetime>=1.3', # Calendar.parseDT - 'psutil>=2.1.0', # net_connections introduced in 2.1.0 + 'psutil>=2.2.1', # 2.1.0 for net_connections and 2.2.1 resolves #1080 'PyOpenSSL', 'pyrfc3339', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 From c1e4b57d374201e1eb19a32357485df11a368a92 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Tue, 31 May 2016 15:53:40 -0700 Subject: [PATCH 129/192] Tiny documentation fixes --- certbot/display/ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 16c44a881..a91c2d747 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -18,7 +18,7 @@ z_util = zope.component.getUtility def get_email(invalid=False, optional=True): """Prompt for valid email address. - :param bool invalid: True if an invalid was provided by the user + :param bool invalid: True if an invalid address was provided by the user :param bool optional: True if the user can use --register-unsafely-without-email to avoid providing an e-mail @@ -32,7 +32,7 @@ def get_email(invalid=False, optional=True): msg = "Enter email address (used for urgent notices and lost key recovery)" unsafe_suggestion = ("\n\nIf you really want to skip this, you can run " "the client with --register-unsafely-without-email " - "but make sure you backup your account key from " + "but make sure you then backup your account key from " "/etc/letsencrypt/accounts\n\n") if optional: if invalid: From 590d816fa9b027cd1252c664eaa74348a7f65398 Mon Sep 17 00:00:00 2001 From: bmw Date: Tue, 31 May 2016 16:03:42 -0700 Subject: [PATCH 130/192] s/assert_called_once/assert_called_once_with (#3100) --- acme/acme/client_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 33e80aab7..a526a0984 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -572,7 +572,7 @@ class ClientNetworkTest(unittest.TestCase): sess = mock.MagicMock() self.net.session = sess del self.net - sess.close.assert_called_once() + sess.close.assert_called_once_with() @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): From 33e905c147ee4a7d731ab128bbac7273c33ec5fe Mon Sep 17 00:00:00 2001 From: Leo Famulari Date: Tue, 31 May 2016 20:37:05 -0400 Subject: [PATCH 131/192] docs: Fix generation of manpage certbot(1). * docs/man/certbot.rst: Fix path to resource file. --- docs/man/certbot.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/man/certbot.rst b/docs/man/certbot.rst index 8fb03db49..d03f3eed4 100644 --- a/docs/man/certbot.rst +++ b/docs/man/certbot.rst @@ -1 +1 @@ -.. literalinclude:: cli-help.txt +.. literalinclude:: ../cli-help.txt From 8d6502a756a8d949fd6016007e61663086102576 Mon Sep 17 00:00:00 2001 From: LeCoyote Date: Thu, 2 Jun 2016 18:17:21 +0400 Subject: [PATCH 132/192] Update constants.py Add strings for Gentoo: one matches platform.linux_distribution(), the other matches contents of /etc/os-release --- certbot-apache/certbot_apache/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot-apache/certbot_apache/constants.py b/certbot-apache/certbot_apache/constants.py index f73004a81..9252814c4 100644 --- a/certbot-apache/certbot_apache/constants.py +++ b/certbot-apache/certbot_apache/constants.py @@ -81,6 +81,7 @@ CLI_DEFAULTS = { "rhel": CLI_DEFAULTS_CENTOS, "amazon": CLI_DEFAULTS_CENTOS, "gentoo": CLI_DEFAULTS_GENTOO, + "gentoo base system": CLI_DEFAULTS_GENTOO, "darwin": CLI_DEFAULTS_DARWIN, } """CLI defaults.""" From 8a8a8b776d403f454d3c8b1afcd766e10a91bb23 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 13:17:41 -0700 Subject: [PATCH 133/192] permanently pin 0.7.0 of letsencrypt in certbot-auto --- letsencrypt-auto-source/letsencrypt-auto | 9 +++------ .../pieces/letsencrypt-auto-requirements.txt | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 2de0652a9..1992c9d47 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -713,6 +713,9 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. @@ -725,12 +728,6 @@ certbot==0.7.0 \ certbot-apache==0.7.0 \ --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 -letsencrypt==0.7.0 \ - --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ - --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 6405efd78..a4af06076 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -175,6 +175,9 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc +letsencrypt==0.7.0 \ + --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ + --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. @@ -187,9 +190,3 @@ certbot==0.7.0 \ certbot-apache==0.7.0 \ --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 -letsencrypt==0.7.0 \ - --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ - --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 From 2659ec3188c0effa0b0f0c6387f582ffc656b04a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 13:27:52 -0700 Subject: [PATCH 134/192] Stop packaging shim packages --- tools/release.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/release.sh b/tools/release.sh index 89a2f5140..c883e3d61 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -45,7 +45,7 @@ export GPG_TTY=$(tty) PORT=${PORT:-1234} # subpackages to be released -SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx letsencrypt letsencrypt-apache letsencrypt-nginx"} +SUBPKGS=${SUBPKGS:-"acme certbot-apache certbot-nginx"} subpkgs_modules="$(echo $SUBPKGS | sed s/-/_/g)" # certbot_compatibility_test is not packaged because: # - it is not meant to be used by anyone else than Certbot devs @@ -164,19 +164,19 @@ for module in certbot $subpkgs_modules ; do done # pin pip hashes of the things we just built -for pkg in acme certbot certbot-apache letsencrypt letsencrypt-apache ; do +for pkg in acme certbot certbot-apache ; do echo $pkg==$version \\ pip hash dist."$version/$pkg"/*.{whl,gz} | grep "^--hash" | python2 -c 'from sys import stdin; input = stdin.read(); print " ", input.replace("\n--hash", " \\\n --hash"),' done > /tmp/hashes.$$ deactivate -if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*15 " ; then +if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*9 " ; then echo Unexpected pip hash output exit 1 fi # perform hideous surgery on requirements.txt... -head -n -15 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ +head -n -9 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ cat /tmp/hashes.$$ >> /tmp/req.$$ cp /tmp/req.$$ letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt From dcadcf8d42c6911a1c35fcd08590f693a68c8fef Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 13:50:30 -0700 Subject: [PATCH 135/192] Release 0.8.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 29 ++++++++---------- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 6 ++++ letsencrypt-auto | 29 ++++++++---------- letsencrypt-auto-source/certbot-auto.asc | 14 ++++----- letsencrypt-auto-source/letsencrypt-auto | 20 ++++++------ letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 18 +++++------ 12 files changed, 63 insertions(+), 63 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index c25cb5c00..cf3aa4df1 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.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 2a4716db7..d85b33f3c 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.8.0.dev0' +version = '0.8.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 5fbef43b1..2de5ff48f 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -713,24 +713,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc - -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 + +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. + +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8d2bd925d..15a2af6e2 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.8.0.dev0' +version = '0.8.0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index bb8b3414e..a710739f9 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.8.0.dev0' +version = '0.8.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index dc0e2764d..98971584a 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.8.0.dev0' +__version__ = '0.8.0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 4026f1cc8..b25326148 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -10,6 +10,7 @@ cert. Major SUBCOMMANDS are: install Install a previously obtained cert in a server renew Renew previously obtained certs that are near expiry revoke Revoke a previously obtained certificate + register Perform tasks related to registering with the CA rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation plugins Display information about installed plugins @@ -53,6 +54,11 @@ optional arguments: to the Subscriber Agreement will still affect you, and will be effective 14 days after posting an update to the web site. (default: False) + --update-registration + With the register verb, indicates that details + associated with an existing registration, such as the + e-mail address, should be updated, rather than + registering a new account. (default: False) -m EMAIL, --email EMAIL Email used for registration and recovery contact. (default: None) diff --git a/letsencrypt-auto b/letsencrypt-auto index 5fbef43b1..2de5ff48f 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -713,24 +713,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc - -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 + +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. + +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 454cbe598..0255229b0 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXSK5DAAoJEE0XyZXNl3Xyyb4H/Ahy9/8ADDaN5V/O/6kl6gE5 -amQfm8T10EUD8APnNWYrYKBYruDBVvH0KiEcuAEs7q4xE5BaQatlobSnsHfv4AWW -TwInk2lRxYZ++MwwQf3DrqMK5QKfcoVnViZsRpZ8gHMLzsJllRm7R5eaTewO2ViM -KM+yDB3UsquLUvE4d3/hgBl2mXAUwsxLeFreZayvpoTcX2ARnzbtKqMaIBYDYWcx -DewWtDsPrhKFpb2DY06S6JLmEttysUgv+hbKlaVO0yZ8cCUehkzBIGYoeS4chOLq -fonNCzB8u3RtnLEFiPIy0N+A592jbLsqqUkxjammaJq3lH7nitduMLnpvGKt4yc= -=ex1J +iQEcBAABAgAGBQJXUJvwAAoJEE0XyZXNl3XyvKsH/3qn7Xa/GQx3HvB6Io/Csn/E +v1nbUg5RPwvrTyyol8BJ6UrHiJw+gTbUgCAnBkZ7DYKaC8AQmQXVRcWXNALMMTzB +6LpBXjQQ2xrBYamGj70N7KnTM1QmxI96GUQouiHMJVugV4uihKJDjtR8/f2JWKok +ZSox6E4LqC45HzqLWiOqc13TrHbti32Mo8DyC63PBnSwMnypGLK6XcqM0L9Re62W +smoKu1VWKwWZYRYXIQr0dvK4JmVTrIsdASdZkhTC/vc8y4tGkdN0DcF2EHzci6OA +Tx0W+Ao+HM1ZcaaH3BJ1y3kYfT+mlt6o4OaK3UB/wtUzMmVih7l1UeiNkVL0oYk= +=t3L6 -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..2de5ff48f 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0.dev0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -719,15 +719,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index e7e6546a25178083f007351bbba85fda56c87bb2..f024577a34a42779e39755635eca4daf6f46ac2a 100644 GIT binary patch literal 256 zcmV+b0ssE80bqRH{`qI)8kxNPxBYvrI}8PM??lS5W%Vrx#G#77ah$EmwK78hzI8fL zpFoy+Z$Ff?%2R#7_m4mldi$VK+OCedjDk7*K$j_4-%~DH(I7GKai+aBwofiSx4Q27 z;SV#~5cO^_zi`sHUA566Xqu@Q&d>p~uV#$A5kiL^Wt5QWl@grCRt$tN>*5-L&1i=@?vr?8a-~$mStKTr+dwJTPqN4;)GA0wY_jCnH7Wesi1I|l7M@y GRi#to9)G0( literal 256 zcmV+b0ssD(3r1$c09~FFRL*(rn=D5xr$W>4tf{yJ0Yd>ifFOs3`#iS_w(uP+p(fG& zTEmPXK+5%ZpiHyaPtmN4i1xvJn%fgm9b2nF4!4kuT8ov;Uq`~S8bQzaVDw Date: Thu, 2 Jun 2016 13:50:37 -0700 Subject: [PATCH 136/192] Bump version to 0.9.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index cf3aa4df1..ed133e128 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.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 d85b33f3c..e3dbe4563 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.8.0' +version = '0.9.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 15a2af6e2..fe2c0c9d0 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.8.0' +version = '0.9.0.dev0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index a710739f9..62c705b4c 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.8.0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 98971584a..34358a5d9 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.8.0' +__version__ = '0.9.0.dev0' diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 2de5ff48f..89d345062 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0" +LE_AUTO_VERSION="0.9.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates From 76a939ceb3c37419e4e9eccde29c558ae332ad56 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 16:00:19 -0700 Subject: [PATCH 137/192] Exit if cannot bootstrap --- letsencrypt-auto-source/letsencrypt-auto.template | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..07f52147b 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -197,6 +197,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } From c9bdc19851f2bae586c27663747723535fe19a51 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 16:03:15 -0700 Subject: [PATCH 138/192] Build letsencrypt-auto --- letsencrypt-auto-source/letsencrypt-auto | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..a5d699995 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -500,6 +500,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } From 6a0c6c85fb7028fdc3793325d40681b7c34f777f Mon Sep 17 00:00:00 2001 From: bmw Date: Thu, 2 Jun 2016 16:42:55 -0700 Subject: [PATCH 139/192] Revert "Use --force-reinstall to fix bad virtualenv package" --- tools/_venv_common.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/_venv_common.sh b/tools/_venv_common.sh index dc6ca3dd2..a121af82d 100755 --- a/tools/_venv_common.sh +++ b/tools/_venv_common.sh @@ -18,8 +18,7 @@ virtualenv --no-site-packages $VENV_NAME $VENV_ARGS # Separately install setuptools and pip to make sure following # invocations use latest pip install -U setuptools -# --force-reinstall used to fix broken pip installation on some systems -pip install --force-reinstall -U pip +pip install -U pip pip install "$@" set +x From 091b6a5cdb996d367dacfe7c93e3c2e604235ce9 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Thu, 2 Jun 2016 23:02:49 -0500 Subject: [PATCH 140/192] Update Arch instructions for the new package name --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index f9af07613..8e691f1e8 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -440,7 +440,7 @@ Operating System Packages .. code-block:: shell - sudo pacman -S letsencrypt + sudo pacman -S certbot **Debian** From 6b7a76442e282e7d926c27cc405dcf10a999652f Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Thu, 2 Jun 2016 23:04:14 -0500 Subject: [PATCH 141/192] Update letsencrypt-auto for Arch's new package name --- letsencrypt-auto-source/letsencrypt-auto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..871c64d0c 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -476,7 +476,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." From ee622618a2cef70b358477e943dff3e375321d7a Mon Sep 17 00:00:00 2001 From: mrstanwell Date: Fri, 3 Jun 2016 00:42:40 -0500 Subject: [PATCH 142/192] Strip "\n" from end of OS version string for OS X. If you don't, it ends up in the UserAgent header and you get an error like: Invalid header value 'CertbotACMEClient/0.8.0 (darwin 10.10.5\n)...' --- certbot/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/util.py b/certbot/util.py index 8507f80d6..35c599737 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -325,7 +325,7 @@ def get_python_os_info(): os_ver = subprocess.Popen( ["sw_vers", "-productVersion"], stdout=subprocess.PIPE - ).communicate()[0] + ).communicate()[0].rstrip('\n') elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] From f5c8a63c1896919056f8a7b7dd53eae0252afacb Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 3 Jun 2016 09:44:12 +0300 Subject: [PATCH 143/192] Tests for --disable-hook-validation --- 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 671da16f0..adbde1d3e 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -651,6 +651,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods out = stdout.getvalue() self.assertEqual("", out) + def test_renew_hook_validation(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command"] + self._test_renewal_common(True, [], args=args, should_renew=False, + error_expected=True) + + def test_renew_no_hook_validation(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command", + "--disable-hook-validation"] + self._test_renewal_common(True, [], args=args, should_renew=True, + error_expected=False) @mock.patch("certbot.cli.set_by_cli") def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): From 2815361e6381a12dd50e5c0fa88bbd6ec3b8775e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 3 Jun 2016 11:12:49 -0700 Subject: [PATCH 144/192] Update the template as well --- letsencrypt-auto-source/letsencrypt-auto.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..73d819b4a 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -173,7 +173,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." From 81cda2903a11b660a88b4b346dd4d9870b9e8260 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 3 Jun 2016 15:30:20 -0700 Subject: [PATCH 145/192] Attempt at putting everything inside Docker --- .../configurators/apache/a2dismod.sh | 14 -- .../configurators/apache/a2enmod.sh | 18 --- .../configurators/apache/apache24.py | 63 -------- .../configurators/apache/common.py | 150 +----------------- .../configurators/common.py | 83 +--------- .../certbot_compatibility_test/test_driver.py | 4 +- .../certbot_compatibility_test/util.py | 10 -- 7 files changed, 5 insertions(+), 337 deletions(-) delete mode 100755 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh delete mode 100755 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh delete mode 100644 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh deleted file mode 100755 index ca96e216f..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -# An extremely simplified version of `a2enmod` for disabling modules in the -# httpd docker image. First argument is the server_root and the second is the -# module to be disabled. - -apache_confdir=$1 -module=$2 - -sed -i "/.*"$module".*/d" "$apache_confdir/test.conf" -enabled_conf="$apache_confdir/mods-enabled/"$module".conf" -if [ -e "$enabled_conf" ] -then - rm $enabled_conf -fi diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh deleted file mode 100755 index 4da9288a2..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# An extremely simplified version of `a2enmod` for enabling modules in the -# httpd docker image. First argument is the Apache ServerRoot which should be -# an absolute path. The second is the module to be enabled, such as `ssl`. - -confdir=$1 -module=$2 - -echo "LoadModule ${module}_module " \ - "/usr/local/apache2/modules/mod_${module}.so" >> "${confdir}/test.conf" -availbase="/mods-available/${module}.conf" -availconf=$confdir$availbase -enabldir="$confdir/mods-enabled" -enablconf="$enabldir/${module}.conf" -if [ -e $availconf -a -d $enabldir -a ! -e $enablconf ] -then - ln -s "..$availbase" $enablconf -fi diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py deleted file mode 100644 index 927c329ef..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Proxies ApacheConfigurator for Apache 2.4 tests""" - -import zope.interface - -from certbot_compatibility_test import errors -from certbot_compatibility_test import interfaces -from certbot_compatibility_test.configurators.apache import common as apache_common - - -# The docker image doesn't actually have the watchdog module, but unless the -# config uses mod_heartbeat or mod_heartmonitor (which aren't installed and -# therefore the config won't be loaded), I believe this isn't a problem -# http://httpd.apache.org/docs/2.4/mod/mod_watchdog.html -STATIC_MODULES = set(["core", "so", "http", "mpm_event", "watchdog"]) - - -SHARED_MODULES = { - "log_config", "logio", "version", "unixd", "access_compat", "actions", - "alias", "allowmethods", "auth_basic", "auth_digest", "auth_form", - "authn_anon", "authn_core", "authn_dbd", "authn_dbm", "authn_file", - "authn_socache", "authnz_ldap", "authz_core", "authz_dbd", "authz_dbm", - "authz_groupfile", "authz_host", "authz_owner", "authz_user", "autoindex", - "buffer", "cache", "cache_disk", "cache_socache", "cgid", "dav", "dav_fs", - "dbd", "deflate", "dir", "dumpio", "env", "expires", "ext_filter", - "file_cache", "filter", "headers", "include", "info", "lbmethod_bybusyness", - "lbmethod_byrequests", "lbmethod_bytraffic", "lbmethod_heartbeat", "ldap", - "log_debug", "macro", "mime", "negotiation", "proxy", "proxy_ajp", - "proxy_balancer", "proxy_connect", "proxy_express", "proxy_fcgi", - "proxy_ftp", "proxy_http", "proxy_scgi", "proxy_wstunnel", "ratelimit", - "remoteip", "reqtimeout", "request", "rewrite", "sed", "session", - "session_cookie", "session_crypto", "session_dbd", "setenvif", - "slotmem_shm", "socache_dbm", "socache_memcache", "socache_shmcb", - "speling", "ssl", "status", "substitute", "unique_id", "userdir", - "vhost_alias"} - - -@zope.interface.implementer(interfaces.IConfiguratorProxy) -class Proxy(apache_common.Proxy): - """Wraps the ApacheConfigurator for Apache 2.4 tests""" - - def __init__(self, args): - """Initializes the plugin with the given command line args""" - super(Proxy, self).__init__(args) - # Running init isn't ideal, but the Docker container needs to survive - # Apache restarts - self.start_docker("bradmw/apache2.4", "init") - - def preprocess_config(self, server_root): - """Prepares the configuration for use in the Docker""" - super(Proxy, self).preprocess_config(server_root) - if self.version[1] != 4: - raise errors.Error("Apache version not 2.4") - - with open(self.test_conf, "a") as f: - for module in self.modules: - if module not in STATIC_MODULES: - if module in SHARED_MODULES: - f.write( - "LoadModule {0}_module /usr/local/apache2/modules/" - "mod_{0}.so\n".format(module)) - else: - raise errors.Error( - "Unsupported module {0}".format(module)) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 9148666fc..7af0ee20e 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -29,58 +29,9 @@ class Proxy(configurators_common.Proxy): super(Proxy, self).__init__(args) self.le_config.apache_le_vhost_ext = "-le-ssl.conf" - self._setup_mock() - self.modules = self.server_root = self.test_conf = self.version = None self._apache_configurator = self._all_names = self._test_names = None - def _setup_mock(self): - """Replaces specific modules with mock.MagicMock""" - mock_subprocess = mock.MagicMock() - mock_subprocess.check_call = self.check_call - mock_subprocess.Popen = self.popen - - mock.patch( - "certbot_apache.configurator.subprocess", - mock_subprocess).start() - mock.patch( - "certbot_apache.parser.subprocess", - mock_subprocess).start() - mock.patch( - "certbot.util.subprocess", - mock_subprocess).start() - mock.patch( - "certbot_apache.configurator.util.exe_exists", - _is_apache_command).start() - - patch = mock.patch( - "certbot_apache.configurator.display_ops.select_vhost") - mock_display = patch.start() - mock_display.side_effect = le_errors.PluginError( - "Unable to determine vhost") - - def check_call(self, command, *args, **kwargs): - """If command is an Apache command, command is executed in the - running docker image. Otherwise, subprocess.check_call is used. - - """ - if _is_apache_command(command): - command = _modify_command(command) - return super(Proxy, self).check_call(command, *args, **kwargs) - else: - return subprocess.check_call(command, *args, **kwargs) - - def popen(self, command, *args, **kwargs): - """If command is an Apache command, command is executed in the - running docker image. Otherwise, subprocess.Popen is used. - - """ - if _is_apache_command(command): - command = _modify_command(command) - return super(Proxy, self).popen(command, *args, **kwargs) - else: - return subprocess.Popen(command, *args, **kwargs) - def __getattr__(self, name): """Wraps the Apache Configurator methods""" method = getattr(self._apache_configurator, name, None) @@ -91,28 +42,19 @@ class Proxy(configurators_common.Proxy): def load_config(self): """Loads the next configuration for the plugin to test""" - if hasattr(self.le_config, "apache_init_script"): - try: - self.check_call([self.le_config.apache_init_script, "stop"]) - except errors.Error: - raise errors.Error( - "Failed to stop previous apache config from running") config = super(Proxy, self).load_config() - self.modules = _get_modules(config) - self.version = _get_version(config) self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) with open(os.path.join(config, "config_file")) as f: config_file = os.path.join(server_root, f.readline().rstrip()) - self.test_conf = _create_test_conf(server_root, config_file) self.preprocess_config(server_root) self._prepare_configurator(server_root, config_file) try: - self.check_call("apachectl -d {0} -f {1} -k start".format( + subprocess.check_call("apachectl -d {0} -f {1} -k start".format( server_root, config_file)) except errors.Error: raise errors.Error( @@ -121,34 +63,9 @@ class Proxy(configurators_common.Proxy): return config - def preprocess_config(self, server_root): - # pylint: disable=anomalous-backslash-in-string, no-self-use - """Prepares the configuration for use in the Docker""" - - find = subprocess.Popen( - ["find", server_root, "-type", "f"], - stdout=subprocess.PIPE) - subprocess.check_call([ - "xargs", "sed", "-e", "s/DocumentRoot.*/DocumentRoot " - "\/usr\/local\/apache2\/htdocs/I", - "-e", "s/SSLPassPhraseDialog.*/SSLPassPhraseDialog builtin/I", - "-e", "s/TypesConfig.*/TypesConfig " - "\/usr\/local\/apache2\/conf\/mime.types/I", - "-e", "s/LoadModule/#LoadModule/I", - "-e", "s/SSLCertificateFile.*/SSLCertificateFile " - "\/usr\/local\/apache2\/conf\/empty_cert.pem/I", - "-e", "s/SSLCertificateKeyFile.*/SSLCertificateKeyFile " - "\/usr\/local\/apache2\/conf\/rsa1024_key2.pem/I", - "-i"], stdin=find.stdout) - def _prepare_configurator(self, server_root, config_file): """Prepares the Apache plugin for testing""" self.le_config.apache_server_root = server_root - self.le_config.apache_ctl = "apachectl -d {0} -f {1}".format( - server_root, config_file) - self.le_config.apache_enmod = "a2enmod.sh {0}".format(server_root) - self.le_config.apache_dismod = "a2dismod.sh {0}".format(server_root) - self.le_config.apache_init_script = self.le_config.apache_ctl + " -k" self._apache_configurator = configurator.ApacheConfigurator( config=configuration.NamespaceConfig(self.le_config), @@ -158,7 +75,6 @@ class Proxy(configurators_common.Proxy): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" super(Proxy, self).cleanup_from_tests() - mock.patch.stopall() def get_all_names_answer(self): """Returns the set of domain names that the plugin should find""" @@ -183,39 +99,6 @@ class Proxy(configurators_common.Proxy): domain, cert_path, key_path, chain_path, fullchain_path) -def _is_apache_command(command): - """Returns true if command is an Apache command""" - if isinstance(command, list): - command = command[0] - - for apache_command in APACHE_COMMANDS: - if command.startswith(apache_command): - return True - - return False - - -def _modify_command(command): - """Modifies command so configtest works inside the docker image""" - if isinstance(command, list): - for i in xrange(len(command)): - if command[i] == "configtest": - command[i] = "-t" - else: - command = command.replace("configtest", "-t") - - return command - - -def _create_test_conf(server_root, apache_config): - """Creates a test config file and adds it to the Apache config""" - test_conf = os.path.join(server_root, "test.conf") - open(test_conf, "w").close() - subprocess.check_call( - ["sed", "-i", "1iInclude test.conf", apache_config]) - return test_conf - - def _get_server_root(config): """Returns the server root directory in config""" subdirs = [ @@ -251,34 +134,3 @@ def _get_names(config): words[1].find(".") != -1): all_names.add(words[1]) return all_names, non_ip_names - - -def _get_modules(config): - """Returns the list of modules found in module_list""" - modules = [] - with open(os.path.join(config, "modules")) as f: - for line in f: - # Modules list is indented, everything else is headers/footers - if line[0].isspace(): - words = line.split() - # Modules redundantly end in "_module" which we can discard - modules.append(words[0][:-7]) - - return modules - - -def _get_version(config): - """Return version of Apache Server. - - Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)). Code taken from - the Apache plugin. - - """ - with open(os.path.join(config, "version")) as f: - # Should be on first line of input - matches = APACHE_VERSION_REGEX.findall(f.readline()) - - if len(matches) != 1: - raise errors.Error("Unable to find Apache version") - - return tuple([int(i) for i in matches[0].split(".")]) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py index 4657883a3..4592eca39 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py @@ -4,8 +4,6 @@ import os import shutil import tempfile -import docker - from certbot import constants from certbot_compatibility_test import errors from certbot_compatibility_test import util @@ -18,20 +16,9 @@ class Proxy(object): # pylint: disable=too-many-instance-attributes """A common base for compatibility test configurators""" - _NOT_ADDED_ARGS = True - @classmethod def add_parser_arguments(cls, parser): """Adds command line arguments needed by the plugin""" - if Proxy._NOT_ADDED_ARGS: - group = parser.add_argument_group("docker") - group.add_argument( - "--docker-url", default="unix://var/run/docker.sock", - help="URL of the docker server") - group.add_argument( - "--no-remove", action="store_true", - help="do not delete container on program exit") - Proxy._NOT_ADDED_ARGS = False def __init__(self, args): """Initializes the plugin with the given command line args""" @@ -43,10 +30,8 @@ class Proxy(object): for config in os.listdir(config_dir)] self.args = args - self._docker_client = docker.Client( - base_url=self.args.docker_url, version="auto") - self.http_port, self.https_port = util.get_two_free_ports() - self._container_id = None + self.http_port = 80 + self.https_port = 443 def has_more_configs(self): """Returns true if there are more configs to test""" @@ -54,9 +39,6 @@ class Proxy(object): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" - self._docker_client.stop(self._container_id, 0) - if not self.args.no_remove: - self._docker_client.remove_container(self._container_id) def load_config(self): """Returns the next config directory to be tested""" @@ -65,67 +47,6 @@ class Proxy(object): os.makedirs(backup) return self._configs.pop() - def start_docker(self, image_name, command): - """Creates and runs a Docker container with the specified image""" - logger.warning("Pulling Docker image. This may take a minute.") - for line in self._docker_client.pull(image_name, stream=True): - logger.debug(line) - - host_config = docker.utils.create_host_config( - binds={self._temp_dir: {"bind": self._temp_dir, "mode": "rw"}}, - port_bindings={ - 80: ("127.0.0.1", self.http_port), - 443: ("127.0.0.1", self.https_port)},) - container = self._docker_client.create_container( - image_name, command, ports=[80, 443], volumes=self._temp_dir, - host_config=host_config) - if container["Warnings"]: - logger.warning(container["Warnings"]) - self._container_id = container["Id"] - self._docker_client.start(self._container_id) - - def check_call(self, command, *args, **kwargs): - # pylint: disable=unused-argument - """Simulates a call to check_call but executes the command in the - running docker image - - """ - if self.popen(command).returncode: - raise errors.Error( - "{0} exited with a nonzero value".format(command)) - - def popen(self, command, *args, **kwargs): - # pylint: disable=unused-argument - """Simulates a call to Popen but executes the command in the - running docker image - - """ - class SimplePopen(object): - # pylint: disable=too-few-public-methods - """Simplified Popen object""" - def __init__(self, returncode, output): - self.returncode = returncode - self._stdout = output - self._stderr = output - - def communicate(self): - """Returns stdout and stderr""" - return self._stdout, self._stderr - - if isinstance(command, list): - command = " ".join(command) - - returncode, output = self.execute_in_docker(command) - return SimplePopen(returncode, output) - - def execute_in_docker(self, command): - """Executes command inside the running docker image""" - logger.debug("Executing '%s'", command) - exec_id = self._docker_client.exec_create(self._container_id, command) - output = self._docker_client.exec_start(exec_id) - returncode = self._docker_client.exec_inspect(exec_id)["ExitCode"] - return returncode, output - def copy_certs_and_keys(self, cert_path, key_path, chain_path=None): """Copies certs and keys into the temporary directory""" cert_and_key_dir = os.path.join(self._temp_dir, "certs_and_keys") diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 6823dfdab..ad5755d60 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -25,8 +25,8 @@ from certbot_compatibility_test.configurators.apache import apache24 DESCRIPTION = """ -Tests Certbot plugins against different server configuratons. It is -assumed that Docker is already installed. If no test types is specified, all +Tests Certbot plugins against different server configurations. It is +assumed that Docker is already installed. If no test type is specified, all tests that the plugin supports are performed. """ diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index cbce4fb56..570bf1a9e 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -52,13 +52,3 @@ def extract_configs(configs, parent_dir): raise errors.Error("Unknown configurations file type") return config_dir - - -def get_two_free_ports(): - """Returns two free ports to use for the tests""" - with contextlib.closing(socket.socket()) as sock1: - with contextlib.closing(socket.socket()) as sock2: - sock1.bind(("", 0)) - sock2.bind(("", 0)) - - return sock1.getsockname()[1], sock2.getsockname()[1] From c79924b7712e2bd770b4c1afec2b2ebb21e32a07 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 3 Jun 2016 16:35:10 -0700 Subject: [PATCH 146/192] Work in progress on removing_proxy --- .../configurators/apache/common.py | 15 +++++++++++++-- .../certbot_compatibility_test/test_driver.py | 7 ++++--- certbot-compatibility-test/setup.py | 5 ++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 7af0ee20e..696ef976a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -9,6 +9,7 @@ import zope.interface from certbot import configuration from certbot import errors as le_errors from certbot_apache import configurator +from certbot_apache import constants from certbot_compatibility_test import errors from certbot_compatibility_test import interfaces from certbot_compatibility_test import util @@ -31,6 +32,11 @@ class Proxy(configurators_common.Proxy): self.modules = self.server_root = self.test_conf = self.version = None self._apache_configurator = self._all_names = self._test_names = None + patch = mock.patch( + "certbot_apache.configurator.display_ops.select_vhost") + mock_display = patch.start() + mock_display.side_effect = le_errors.PluginError( + "Unable to determine vhost") def __getattr__(self, name): """Wraps the Apache Configurator methods""" @@ -50,12 +56,11 @@ class Proxy(configurators_common.Proxy): with open(os.path.join(config, "config_file")) as f: config_file = os.path.join(server_root, f.readline().rstrip()) - self.preprocess_config(server_root) self._prepare_configurator(server_root, config_file) try: subprocess.check_call("apachectl -d {0} -f {1} -k start".format( - server_root, config_file)) + server_root, config_file).split()) except errors.Error: raise errors.Error( "Apache failed to load {0} before tests started".format( @@ -65,8 +70,13 @@ class Proxy(configurators_common.Proxy): def _prepare_configurator(self, server_root, config_file): """Prepares the Apache plugin for testing""" + for k in constants.CLI_DEFAULTS_DEBIAN.keys(): + setattr(self.le_config, "apache_" + k, constants.os_constant(k)) self.le_config.apache_server_root = server_root + # An alias + self.le_config.apache_handle_modules = self.le_config.apache_handle_mods + self._apache_configurator = configurator.ApacheConfigurator( config=configuration.NamespaceConfig(self.le_config), name="apache") @@ -75,6 +85,7 @@ class Proxy(configurators_common.Proxy): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" super(Proxy, self).cleanup_from_tests() + mock.patch.stopall() def get_all_names_answer(self): """Returns the set of domain names that the plugin should find""" diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index ad5755d60..ce9f590d5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -21,7 +21,7 @@ from certbot_compatibility_test import errors from certbot_compatibility_test import util from certbot_compatibility_test import validator -from certbot_compatibility_test.configurators.apache import apache24 +from certbot_compatibility_test.configurators.apache import common DESCRIPTION = """ @@ -31,7 +31,7 @@ tests that the plugin supports are performed. """ -PLUGINS = {"apache": apache24.Proxy} +PLUGINS = {"apache": common.Proxy} logger = logging.getLogger(__name__) @@ -47,6 +47,8 @@ def test_authenticator(plugin, config, temp_dir): "challenge types") return False + import ipdb + ipdb.set_trace() try: responses = plugin.perform(achalls) except le_errors.Error as error: @@ -341,7 +343,6 @@ def main(): temp_dir = tempfile.mkdtemp() plugin = PLUGINS[args.plugin](args) try: - plugin.execute_in_docker("mkdir -p /var/log/apache2") while plugin.has_more_configs(): success = True diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8d2bd925d..07dfa1684 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -7,9 +7,8 @@ from setuptools import find_packages version = '0.8.0.dev0' install_requires = [ - 'certbot=={0}'.format(version), - 'certbot-apache=={0}'.format(version), - 'docker-py', + 'certbot', + 'certbot-apache', 'requests', 'zope.interface', ] From 91cd19158e4f080a5f013b2c8af772f9d7004157 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Sat, 4 Jun 2016 22:53:51 -0700 Subject: [PATCH 147/192] Improve user experience for linting. Don't run pep8 for directories that we don't actually enforce pep8 on. Install dependencies with -q. Don't print reports, they make it hard to find the actual errors. Remove deprecated fields from acme .pylintrc, they cause unnecessary messages about deprecation. --- acme/.pylintrc | 6 ------ pep8.travis.sh | 12 ------------ tox.ini | 14 +++++++------- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/acme/.pylintrc b/acme/.pylintrc index d0d150631..06bb2a01f 100644 --- a/acme/.pylintrc +++ b/acme/.pylintrc @@ -21,12 +21,6 @@ persistent=yes # usually to register additional checkers. load-plugins=linter_plugin -# DEPRECATED -include-ids=no - -# DEPRECATED -symbols=no - # Use multiple processes to speed up Pylint. jobs=1 diff --git a/pep8.travis.sh b/pep8.travis.sh index c13547a78..cadea8489 100755 --- a/pep8.travis.sh +++ b/pep8.travis.sh @@ -2,16 +2,4 @@ set -e # Fail fast -# PEP8 is not ignored in ACME pep8 --config=acme/.pep8 acme - -pep8 \ - setup.py \ - certbot \ - certbot-apache \ - certbot-nginx \ - certbot-compatibility-test \ - letshelp-certbot \ - || echo "PEP8 checking failed, but it's ignored in Travis" - -# echo exits with 0 diff --git a/tox.ini b/tox.ini index 5c88dfd21..cb625ba8d 100644 --- a/tox.ini +++ b/tox.ini @@ -64,14 +64,14 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot + pip install -q -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot ./pep8.travis.sh - pylint --rcfile=.pylintrc certbot - pylint --rcfile=acme/.pylintrc acme/acme - pylint --rcfile=.pylintrc certbot-apache/certbot_apache - pylint --rcfile=.pylintrc certbot-nginx/certbot_nginx - pylint --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test - pylint --rcfile=.pylintrc letshelp-certbot/letshelp_certbot + pylint --reports=n --rcfile=.pylintrc certbot + pylint --reports=n --rcfile=acme/.pylintrc acme/acme + pylint --reports=n --rcfile=.pylintrc certbot-apache/certbot_apache + pylint --reports=n --rcfile=.pylintrc certbot-nginx/certbot_nginx + pylint --reports=n --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test + pylint --reports=n --rcfile=.pylintrc letshelp-certbot/letshelp_certbot [testenv:apacheconftest] #basepython = python2.7 From 08ccc64cd1dc0c852ad4189bbc37aa451e7a611c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:04:44 +0300 Subject: [PATCH 148/192] Initialize augeas in a new method --- certbot-apache/certbot_apache/augeas_configurator.py | 4 +++- certbot-apache/certbot_apache/configurator.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 12753541c..820a58438 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -1,7 +1,6 @@ """Class of Augeas Configurators.""" import logging -import augeas from certbot import errors from certbot import reverter @@ -29,6 +28,9 @@ class AugeasConfigurator(common.Plugin): def __init__(self, *args, **kwargs): super(AugeasConfigurator, self).__init__(*args, **kwargs) + def init_augeas(self): + """ Initialize the actual Augeas instance """ + import augeas self.aug = augeas.Augeas( # specify a directory to load our preferred lens from loadpath=constants.AUGEAS_LENS_DIR, diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index e4c06ba7e..9caa4a764 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -150,6 +150,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :raises .errors.PluginError: If there is any other error """ + # Perform the actual Augeas initialization to be able to react + try: + self.init_augeas() + except ImportError: + raise errors.NoInstallationError("Problem in Augeas installation") + # Verify Apache is installed if not util.exe_exists(constants.os_constant("restart_cmd")[0]): raise errors.NoInstallationError From 7239361342e0001cfc8ad958250564e50e8bd0cb Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:36:54 +0300 Subject: [PATCH 149/192] Test coverage for NoInstallationError --- certbot-apache/certbot_apache/tests/configurator_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index a2e39de47..57344bbb6 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -55,6 +55,14 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.augeas_configurator.AugeasConfigurator.init_augeas") + def test_prepare_no_augeas(self, mock_init_augeas): + def side_effect_error(*args, **kwargs): + raise ImportError + mock_init_augeas.side_effect = side_effect_error + self.assertRaises( + errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.parser.ApacheParser") @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_version(self, mock_exe_exists, _): From e2631322839e4a3b5117fb4581da72fa4c2eddc5 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:44:49 +0300 Subject: [PATCH 150/192] Refactor and lint fixes --- .../certbot_apache/augeas_configurator.py | 20 +++++++++++-------- .../certbot_apache/tests/configurator_test.py | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 820a58438..478efb099 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -28,6 +28,18 @@ class AugeasConfigurator(common.Plugin): def __init__(self, *args, **kwargs): super(AugeasConfigurator, self).__init__(*args, **kwargs) + # Placeholder for augeas + self.aug = None + + self.save_notes = "" + + # See if any temporary changes need to be recovered + # This needs to occur before VirtualHost objects are setup... + # because this will change the underlying configuration and potential + # vhosts + self.reverter = reverter.Reverter(self.config) + self.recovery_routine() + def init_augeas(self): """ Initialize the actual Augeas instance """ import augeas @@ -37,14 +49,6 @@ class AugeasConfigurator(common.Plugin): # Do not save backup (we do it ourselves), do not load # anything by default flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) - self.save_notes = "" - - # See if any temporary changes need to be recovered - # This needs to occur before VirtualHost objects are setup... - # because this will change the underlying configuration and potential - # vhosts - self.reverter = reverter.Reverter(self.config) - self.recovery_routine() def check_parsing_errors(self, lens): """Verify Augeas can parse all of the lens files. diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 57344bbb6..e5c09fd1d 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -57,7 +57,9 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot_apache.augeas_configurator.AugeasConfigurator.init_augeas") def test_prepare_no_augeas(self, mock_init_augeas): - def side_effect_error(*args, **kwargs): + """ Test augeas initialization ImportError """ + def side_effect_error(): + """ Side effect error for the test """ raise ImportError mock_init_augeas.side_effect = side_effect_error self.assertRaises( From 1f6e999153be1347d92eef884a297966c0e15801 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 13:10:34 +0300 Subject: [PATCH 151/192] Move recovery_routine() to augeas init --- certbot-apache/certbot_apache/augeas_configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 478efb099..6999120d6 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -38,7 +38,6 @@ class AugeasConfigurator(common.Plugin): # because this will change the underlying configuration and potential # vhosts self.reverter = reverter.Reverter(self.config) - self.recovery_routine() def init_augeas(self): """ Initialize the actual Augeas instance """ @@ -49,6 +48,7 @@ class AugeasConfigurator(common.Plugin): # Do not save backup (we do it ourselves), do not load # anything by default flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) + self.recovery_routine() def check_parsing_errors(self, lens): """Verify Augeas can parse all of the lens files. From 78ea886a79c3dc3b1c75a5d5eb3dca5abbd15219 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 11:49:36 -0700 Subject: [PATCH 152/192] Fix deploy cert and TLSSNI check --- .../certbot_compatibility_test/test_driver.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index ce9f590d5..885eacd92 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -47,8 +47,6 @@ def test_authenticator(plugin, config, temp_dir): "challenge types") return False - import ipdb - ipdb.set_trace() try: responses = plugin.perform(achalls) except le_errors.Error as error: @@ -63,7 +61,7 @@ def test_authenticator(plugin, config, temp_dir): "Plugin failed to complete %s for %s in %s", type(achalls[i]), achalls[i].domain, config) success = False - elif isinstance(responses[i], challenges.TLSSNI01): + elif isinstance(responses[i], challenges.TLSSNI01Response): verify = functools.partial(responses[i].simple_verify, achalls[i], achalls[i].domain, util.JWK.public_key(), @@ -144,7 +142,7 @@ def test_deploy_cert(plugin, temp_dir, domains): for domain in domains: try: - plugin.deploy_cert(domain, cert_path, util.KEY_PATH) + plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path) except le_errors.Error as error: logger.error("Plugin failed to deploy ceritificate for %s:", domain) logger.exception(error) From 1d3fbe945de27894578fdc79878b4ce5032edaa2 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:01:55 -0700 Subject: [PATCH 153/192] Copy config into /etc/apache2 --- .../configurators/apache/common.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 696ef976a..af27f7ed5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -53,14 +53,15 @@ class Proxy(configurators_common.Proxy): self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) - with open(os.path.join(config, "config_file")) as f: - config_file = os.path.join(server_root, f.readline().rstrip()) + # with open(os.path.join(config, "config_file")) as f: + # config_file = os.path.join(server_root, f.readline().rstrip()) + shutil.rmtree("/etc/apache2") + shutil.copytree(server_root, "/etc/apache2", symlinks=True) - self._prepare_configurator(server_root, config_file) + self._prepare_configurator() try: - subprocess.check_call("apachectl -d {0} -f {1} -k start".format( - server_root, config_file).split()) + subprocess.check_call("apachectl -k start".split()) except errors.Error: raise errors.Error( "Apache failed to load {0} before tests started".format( @@ -68,11 +69,10 @@ class Proxy(configurators_common.Proxy): return config - def _prepare_configurator(self, server_root, config_file): + def _prepare_configurator(self): """Prepares the Apache plugin for testing""" for k in constants.CLI_DEFAULTS_DEBIAN.keys(): setattr(self.le_config, "apache_" + k, constants.os_constant(k)) - self.le_config.apache_server_root = server_root # An alias self.le_config.apache_handle_modules = self.le_config.apache_handle_mods From e0bb04fd2550701d5b553a1f1516fd17a096d611 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:02:53 -0700 Subject: [PATCH 154/192] Forgot to import shutil --- .../certbot_compatibility_test/configurators/apache/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index af27f7ed5..918db5f47 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -1,6 +1,7 @@ """Provides a common base for Apache proxies""" import re import os +import shutil import subprocess import mock From e1f4e22c6d5e158eb213745af571cba29dde3e86 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 12:09:17 -0700 Subject: [PATCH 155/192] Unwrap achall --- .../certbot_compatibility_test/test_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 885eacd92..165791684 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -62,7 +62,7 @@ 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], + verify = functools.partial(responses[i].simple_verify, achalls[i].chall, achalls[i].domain, util.JWK.public_key(), host="127.0.0.1", From 144dbdd90b0637777163597e338c4872c890e6e3 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:23:15 -0700 Subject: [PATCH 156/192] Explain whether tests succeeded or failed overall --- .../certbot_compatibility_test/test_driver.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 165791684..fae2ed2c7 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -7,6 +7,7 @@ import os import shutil import tempfile import time +import sys import OpenSSL @@ -341,6 +342,7 @@ def main(): temp_dir = tempfile.mkdtemp() plugin = PLUGINS[args.plugin](args) try: + overall_success = True while plugin.has_more_configs(): success = True @@ -359,10 +361,18 @@ def main(): if success: logger.info("All tests on %s succeeded", config) else: + overall_success = False logger.error("Tests on %s failed", config) finally: plugin.cleanup_from_tests() + if overall_success: + logger.warn("All compatibility tests succeeded") + sys.exit(0) + else: + logger.warn("One or more compatibility tests failed") + sys.exit(1) + if __name__ == "__main__": main() From 8723bded727a9901314187427ab0e07fbd43b586 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:17:11 -0700 Subject: [PATCH 157/192] Add extra saves for apache plugin --- .../certbot_compatibility_test/test_driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index fae2ed2c7..38abffb18 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -144,6 +144,7 @@ def test_deploy_cert(plugin, temp_dir, domains): for domain in domains: try: plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path) + plugin.save() # Needed by the Apache plugin except le_errors.Error as error: logger.error("Plugin failed to deploy ceritificate for %s:", domain) logger.exception(error) @@ -178,6 +179,7 @@ def test_enhancements(plugin, domains): for domain in domains: try: plugin.enhance(domain, "redirect") + 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) From 1c1816fb4a49e97b99300d50be00df5f41bb508d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:35:28 -0700 Subject: [PATCH 158/192] Update tarball --- .../testdata/configs.tar.gz | Bin 101287 -> 100286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz index 7323acf747643887d8883f3ef74437d811032b2d..05f7f4f9bc54975c0f575ebd80613c9e419dc3b2 100644 GIT binary patch literal 100286 zcmXuIV{j%+u>Ku88{4*R+t$XmZQHhOJ9joVH`>^?dB4v&=l_11o|>BOtA1V8)73*1 z3j@-1si6e|e)Zee^PhCaZQ#C|`qkt}0gGePvY>SZtWl&jxWVB0Q^TnfTy;>)vynO~ zXZH5(!0S#*SlA!M7DXS=P2NgV^+Y|5*Y{qHa?K?JZIiyd-YKT9JNp7seyVodABOzD zF_VDBN3rVPwHkDzF4x%q(q*aA{~^`A#Gj=4fE~LgHWN|4T!&s0)&hk-s>;3)*RrQJ zyl5Hj(@cwk5^GnsADC89AA~^Pwd9T;km}yjQrDlq{v6L^ZDqx}5K0t&_ftK7`Mpw^ z13PS?GL)4B#>id_X|?2RKN~d%f9TRJKY@bt;nNDnEe#o8))_brRAag*>!5dInPbKo zG_L_046pIs=W{n}GyZ|jfWILX#$g+pRGkRT=mjYat^iV}qkL~b7G8uhoMbcBmZ8>s zHQ^rApbsT1AEu-?#rtYciIJ)O+tvpM6 z7r>le&4GkG@Az9(I}%{8uJ=#x>>s=@ea6s63xw3nZ^IDrl#p23^gClWo^4fdQG^@0 z(^t+U;HlCSFw$tR`Z0)U2$)X$0c8F19s};~Z_oxlE%6K-EghiAeI>5!Pag;V=I8Ow z0KR$&O8p?mWsq9Vm^qLVOra0h#f?7zQV(Qwe~wk#wf)BecMrXnvu&=%iSH6%iI3xb zI8zy+H`J^vC@Pjv>b7nB`mGWhBSP;bPOrPXcW?mLPeocJ7n<`9#l`^y$TKTScoXAvIyrw3?2{8G|z%b(hd0Zhh!FI&}b%WvqzPE{>ld`~uc+tF8pW<=>D) zIHN!R2GF45W?Z%hAC7%#9EzQ~TA0dQOL>=n>dE>T!A!3>kbwwB{Db+Jx!zm%;nA+t zBdJxFD;JQft<~wbt>tY>*}6P}fKQoR%EuKYjS7c*qVHwX1iG(ADVai3V1p2rK|)Sd zUc@>N$W`GrEMY6~LB@XghfZS)=-UBw1YWf+^mL(FC}qVqS6?>2EXoOQwKeD zD6Ad=i-%3DJ?^>maZ2SQ$n=93O!d8@6&Bz_*bKDkcIz_+jvl()Z~OqpTahr=Qs9p9 zHvqRK_G}Z^EFQ(?p**yP>+)MTCi|(bgP0cBV`U3Jpwc39zv?kF*vlO2neVzghs?UT z_K#b+*R2>a!2KVB1lzK_LUyT1D_SKVMG2$+^XP<0PMT7GL*VYy6$IACHo%+B+9ZL0^IR<{^KoXq zFHD5|T*bni=t$Q28s?3RFzpjIdQlHp@C>%dDhuJu96Y?B&lIe?%&fg|+| zoDAH|@5D=HDwkq5Vdm}g{Rx)xE}~kvcf~bm*0KyX?+4Re0$GF^S7)YKfX_V|dK*D} z%K04LvT7HW-C|M6gI}OR(?WLOJhi?Nv>}Ls)OMZ1MzsXO`tKJuxRuQ^e8Zz7{X8T- zy)L;bocX~AOPnz_O_gh(Kd4CeG? zgbnopPsgQC*7V2cFA5*Hv4mz9m~4i+U>rP0RM&F?ZhTbZ1Ok+cPLNfY<5By+;u_MT zcqvHMdl-ZWHtHJ31pln0O@m(eViTl57rE|1_hGofIc}@gHosT&cNesGjM?c3Yca-d zi5Gup3ZeWVfHh+Sb=8dna zVmFv;&4siuSk5OtXx5Zl&SiX6It8{A1WfK{@{>j26gM0z1$gynxpj~e{Ac zVvXpdWi#(u|CSSWbL_5VKp0pm&Pn&Khf8hF*YRDowf9o81)JC;8p{L5&H6?5hX*@9#6kVPE@MUzNISNBhB*SsRYDKIPdF#bZA92vaeEC87T1)+g@4SyKk zWwLnSz<8FrnWUiz{{Y92Wu4i8Od4|YZ$MxEAWNEoX(jX!x3t8&!lk#|uVN~YYhAx{ zA2@z~I)aw)(Pk(UyuM5%oNCyom0&P^mE$B*BI8VPo*w=(JSgO{wJ~Qs6}uXYSUbnI zF-1-)b{1#-FD5|e#(-{JO9(?cm}5raXGeSeAA>$5><)Y}VrB-APIz1RQPzBuLQJ^M z&dztIilRpkP}2O8Z%O`Lae0h{D*t!$q`PD6T|ZCC1%D!u2e6=RW0IWZ36wgU)Kgsf zFIwunO96%5_zh@kh!)%n)j<%B05QNNd-PXIBdiEhQ7Fr95IN){NDyR_x4h=RI8Y)Q z$c!1gt;rdkq!oy_9tXrfWO>lYa3TZ>P&#{ZLU-0!Fl?;z`d%rAd@#Qo_qc*Ey9}bp zU2010xJ!J(^&wunOwX=oJbx{{E|EE=Pc)4Ee zFhc@Cm4?iqxj$l$?>K#wfZPRwV3ft*1W67P9nTZ7hN-Mb0ufil#VXn;1i{7z-;|Gm zi>^#Q=n4&kOcJTd((-&SE`PrS=I)bjLNa~&*cjEXLRlycJM8>C1=E+M8XT!@y~E+Q zmAFui1`>211+Gm{*_eg_(i1!pO8Sf((4_WO-3}Hm%pbWH&rjTzKUTDMo?p0x zpwHN)j+RLpeUkc?vZDIjV88K)8Dyl^*Z$Os+iEh zzO&qypcKnnJb%^gWmk47O!ko$TDa(++l9ESLT-l|4fmLwH;aYPr!SrRQa2$-U|fLd zQQYU*ZyRooRV)Q~DwEq(+!I5Phr~8VsFL{Q6-J@=)(ty?sO1^@cMkdW!$7nI;jjSo z8I~MAhhnmxWwv~lT33FwrZaxkYvHG}_QeVP^R9kdw%S3fe{AJUKXBpw)#GwMAYC2M z`m<+WxnlE5c%!c14%P`m(CCvGr!MDy(P13GaIx9dbq4%c=QphMiKNF#}5s1J^*`;03zW13ql^f_m&62!iR4qBxLyHr}%?3u?0T!+&&(d zSOC;gqRl-G+?y(V1m527We}}Nk)jO7Cb6D?`m7)Kz+_C6F41iz|JGqk%gfp z$t^VSShB5gWV1si?mfkWzl9|{G1FA!1-U>Hi%RD{n1gyD-@6pJ`ynoEpUwPA9jeNi zB|i}}wm?f3VW#&}wD$dla)GMEbYo|`J%7qJl=iwh)F5fuy%s2r)xPqB)o>a(3AIZ$ zZ~9``>j`YtY!KW9zB4NMc|EZx?-R!D2nZ7hgAau&@CHzp8)dCtn3A<9$?d@Ko@zPR zO**8=yEKB$lR=6gL`%Iw?&CF2E$&M)@|#izsU8j0Lw=jFmwG`pFqOZ>{Vj9Yb{d1I$`3dG-943}C8FeV;S1Q=MTgq44^@ zO8!|FJ7hr_xeyKAZAJc(N<=HPK;`W=4qD6dk1*|*bL+xMf;YA!-gzu03}{U^t^+A_ zhZq&)9s7Cwb*AEm*>SAZXW|@2F!vjLu@b2>_+ixb%Ilz5me*ut0x`ZJuZY_|l}LkM z&Ajb)b%EeQ>-mL!jm9xM@3ux_TU|j5W7!^yiW0Sx@HuCDlR_NLfyZFY9q$CmThdml zAT#tvx_R<3?^HbM$ms0RtvW3IaY)jLeh3ZrAKxFVIt6un0qhejCdM4;asc20#y6$ zTi|7;6iWz=L;#Is%ho0FN+)^v0 z*!FRkGTkg}u<)DHGGcfCnPA5EcI2ck%^#H$0d|fBCr3rIc=Ug)^^Gkne!mZl$K&lw ztW}|YtB=l6^LI6QwtM$U@bjvh*|`ev_8->gbdhARgTRH6uY-PZn=p@{@Cp9hM{Njx zf#DE->iuqI$=vFloaYDSfjf)6oPGF%*hT*`<|g!G2Z4r6hQ5k&RAg0-L~RWE zQJaWm6lKz!LPvNywbE|ki8p+YVX)ba17N~zeOBeDsm6-QCf5@-9nMUe<(VbZwv@lF z(W*0N)%OO!3)O$-O(sGV3RQR5%Q|Xpxx{H3-V|Tkx9qLFa)sL7ZOl|Ws5Tf@m+IP4 z>v&bYHg7#v)z)HWU1N%v73p2{Z}#V)S&5z)|7LUS{nqs6z6cOGRlR@C0lXVN@U3^< zZOuxo#PVgPk$xOxVks#;U74tFi=@p#0&*@cD=T&F2QIIRa_%S%WuM=2mHodJsyOu; z<@l<4xCA>j3xiIymTrKoHvAOV{;RJ3{n4cOTv@Y^8~)gh_>IQ|Zc2(lzvrj@bdH%{ zUBX7^P+3fv3PyG?$5YDGcpYv``jHCW={D48gfKL=TwAcb*hmSw*Mpys(9TMi`>!10 zv=hJEC~I-_8!_3C+fS0VELfR}w1EI{a)XF&1~`Q1tmsh6ib$Mk135 zrS#mvqUpvVvQ!g9{H~zRf!@hAw6_g4R}D2c4K>#dygT~tDGjnp%`KdE4vYwRJPS5l zhqZIiq(alC(QutDwxcHZNeybw{8E`)(M*0LdCRCEvT2kMUazw$g4_l8*rWl*s8oJl z-Jp&$P9MZ#d!F>g{O-E&?7w0>yAGXIX*!+teE5oo{V?^F(zGN9SlhI1)zm$C)Udlb zD(e)2uWgjQ>qMTLq1wlB@r73R%=ExzYlRL)BIB@v2NekN{+mfqtXk=F_C{KB+R?LV z#&yzdiBX@>Qr}CSUR_1~%yJ~I7j9hceyj{BK7!wFp5KMQr6pCzcx`8 zM-{WOwQM|jF<7Wh%9@uHs@u4#1V|1^hpiZSzY`e(>8Z%I5esJ#UuF9WJK-*5N*uPN1pRKl24kK5hSLt#?1djmJ_8rTd1z zL0St_9AvJ}2Op@5(%pGgy#+^xOXH0|XF=>HBHK~bYN&lu+=2mhnR+-2CqkH>u^NF@ zbst$J6hygQ>|wo6pFbm#{HNe#Fl$)dWNk!QUqZ}O@qQ{Ae{0%Z+*67Zw`kTBz{cEP za?azS0A?N>;3MTUJLQ^=i?Y2-cfXGn{aOaQE^Q z$+5U8w^Jyk@0a?w6N_wiC#6>mJA zAMHJN^NFK)s_lA7@=+ynxfMJLQf*koMLgUo{yMB&yvU>Izw95#Z>=YbB~8%d!+agP zHqgZDjvcG&-C%NnhFd``-~Lv{Gukg&r)zqX4U4t{ccsIEFcn%v8W34Fh|=)@`06aa zI`h@T6}(eEhG^nbyXmbK&cfZ^htj26L4^;qv$|QfI)Wk~_IUPV;Bcy(TM~Aj)A5fm zIDqgfYR#vQ`4chrA_$j&XPs`Lv2i+y>{edDe67?2U5#dpU)Z>qWE%xV*1oIHZRz@fsg+rjyZ;n=z}8Pv!4hgh5$(>vxU%qP|2*Roup*|paFtGUNG?cRd+0%y8xC|N|QeW z%8wltj6ESAoFra%g7n^b9aFnWEo2cg76mG5swIl^s{T5VGB4OBK@+Ctozz5+DF!jy`>dnDX)Z0W{h*NY*qQOG8dkCKLcYmGldXiR@4G#BRKf(0}=bj!idcx@s zwA!F6v%JYLcFbP^v!;m71H%u9tZhr$vrsqu2t!2{M~DpEWp2Pb*f=hQm*0RsU!HSq!oQbI3S()nZ3 z(22+HtoAbUN4no-QT&PU;+IUf$v>$+Wq>*<&7>&2RzR1Orfagu(fjWi+}6SsGNnUO z3G&d9)fzz=PnnpB)E;t)Lmu>lG5G~AN+cZi6B@f<#?^9aL99~#MI959(5TY#3yu9b zRWLK5uC3cvq_f7o$9vJK;$Qy z_5eotm(@yCiH(_mN_~(~ZR9GL$vkoR_1YLY@77e=JAK1JamM7DD_vLEI+K=D!O0^X zHyRzyTtImUeAKrlq#8S$RsA@xh!k)3Dy-wdqaQ>tl&LrOR0jbUD<8zrEqe%9{@db< z2MzodqX0*}Qzjy%fN~Kq+v#Uwzgft+4wRPl;Pda5+l;<^Bve)w65Uk4czCr3s#%bq zRC+13@xEe^qhur3;@x2~NcUmgFi(W_uF5J~Tc?B(q%E51C*Kz9z1`BUZ03GY1V~9V zWC6)u97@MhW%rrg7u3TK@Lsy#T%l)i(2f#O+3JXjXX6{3vgv7WaU#Jm9mJc0fRB+X z8Wx7EiOl@2DXW%@Q#Nc^Pya4V<#RmF-8VYZ?k{`IlIGhXLO0wuJhDcQ!OUR^;qr5;(`9Ay8*D+5JEKxAyFDTp5m9MQTp-A9PM}#(wlR zI6Sd|_|x+`;uR{f+h-p+n<92|YouHkM;WyPzs&dRzml|CQ)edKb?LgIlFIdp((((= zgbsKi6M6zc3NeY^>|N(Y<8c#RqZ?Ftt`Bx}bIK@sH)1H5p?|*KNTa@vSj@-M<)V>| zK9`HcY0!D0!{vCw*_c1{$_i`;9cp87yd)gp-Zgt%)M(U_JV~buyebi@qC>r}r~|Qb zDhpfXHdKF?wMF*R68gzC)B2>@7qZ7@_Fg^pw6h`kNBpR!Eb~~U6_Y2ijjR|n-1H`% z{fc{LgY$us=Kw9mVvo~C9+Ib1kNP=tzVje#tvhBM=r2S&$CIS+NSRf!gZYS|2#4gK z@pdp0G6T)zxK9oL>q%K7wM6)n8PUqWFG+;7t6E-T33?pjJ5#c(k>CFFJ@%BiG^i?F zlX^ai=jYW1qRjJrrRuZ7E^*J!l-kf>92s=b@9mw{%-0p1;De-AL4!6hBV)AI;XMn|mIpnHLB=BSOaE#wq(w{cE18;Zf*M{H4t2%0 zRlZ6DtST3ASpfuRLs$~5Dh*Lx7R0=r0)#@>C21uEL|q0%qcyk&wy58(NMH-Msxtpi z1R?xlJ*WNFKZHPeZ~A`;#8?hoE=y0nzuIG111v7o(@Gwf!~?ByurROrCbi_dSXBa8 zAX}!#n=+XN2I#=bQuxGXeoM|N_qwDlSqs$txx204I*+qU)6G3&2Z;$4t2SF9SBf;D z!|R1m%l@xVTEtMVCO0enVdpOMvQeYuTP_qt956R-Q{Hyh3^g<;TO0K*%DMcdC8jdD zVPuOjCB#YHj025m3sf{W+!L@WNXexsq1(A zK?&^3efiYiMYuwdtspDUjnzb?#q*6tBO5{raWR}7zMkRgr-vk3Lo^I!Q+JmYQ9-iz)$F8L{0 zr;k~ZP!_wV%j`C;gzag;7aK^{@+*3+1Q_DTC2JpS%ny1(O8QV

>+7bOwi=X@0cTd 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 z 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 literal 101287 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

>PD40$S_DOkt_8cFeaIjOMKd6SRvO4frMW zLR)A>ZZ<5Ugdk`X9h?XbHa)hkx(#yFG51@Ni)C}@H?7IVgCf9$&rs$NM6&lIs|N{J z?T&!XKg7Vo*%q#qD6xjD^w@PWJ=2u`F4S1Hvh|gKdj`|DCxw>S z^gcsesng~)q)r`6Ij@C9p~S8BCAHgEZz6@dd0$dYBO?Sq4H5$9UQC5k3&lpEv{H=2 zb#7`xrtpE>+1k8(eo(;c&U9XDGkQl;Y}P>cGO( z&nq5~fWlmvMDJZm-g>c#H%4YM$G2p@P6l%=h;B>OHPOv2vNiK2q;~pp3JJ+*hlF$u ziZ;_6HM4Ne#2PB=FNiJF3mGHANrT_YU_^&9lK^$igHZ4_IEYR)a&m=!Kv?%%6X);hZvvyph{yyxe}^n z>(_CdK{@zDPsu;b+0!U=4FWfb@gjdc$;C2QNanvtHw1_aMmKzF#N$~imQGP*2&jrD zLpN%e!ra_b79xzk#uZf$)V7j5+q_7zV+{LALYr0)K_%$#E8^NQwvBGN`%;<5p`R=0!HN(wOFjucwiH`5mLAO^D9;1~ zrw=Kc7-o6=@`>Ss&=u3>%U-$K%cz9)58)WfVCPx!-UxKtuaQ@&V7dng*I>^+nsqDv z?{_GLo6~eb_VR6En!qwL2VjdB@qVBHHFE>z6vY9>Qzo z6@r8~<5jsszg!fO-k+YZIyJIgYMciGWuIr6pR=Bon(iDX_O{~3uG=k|DwgdpOj~Cj zfBV<8>rXA5*RDNHHa8bH&GXPd;X~>Lgf&&5QY87sRGvU=z&2EHWt>f9YzkgAB+E@- za8;hX-Jd--485e>madgnRb3d4ShTs#y-165^AmKA;`k~!^55OKpX5*3*6Y2E=oYk; zc(FMr-)UA&y|irDHoc7L9@s$N{vQ17AP{3OKnvff`f$HrxOOzh0%464qucRtGqy}T z94NGE1zF`Vm;UC^c2lx`v>=e>Zs`aAGzn+>b%XQ7^QzV#aim2o!oSj{?fGDN;w0lq zin3 z`%CXI3{{oAKAZ|SPQ@O_J32T2xBJQJzA3l&7Nhueo5>0*Nwg0B=U_!zNS@*;Q#aar zrW!)Z2g{HxQ7bW_oxqdix=}gvJ{>*vaVLG19yY#W`P|-!Ih-?@33mmA$qA{-PJHv% z;q1Ox=7V&1N)3zX6AE=sV?+7S<2)4Ql5U5UHCHqyG&sNUm_?%Z5Sek7=?Ke!{Q>h>YV`n$BfR1>2ORNdE0OEwbK?}DsEpCA?` zwGo?5^76V{?Z&m~HhSE7l##E$-wEyAA8DVR#2?%64~ZrWC@kLvD{(NTaRYB zIH`;7%^Fj=XR%R|I9hYTTVZA-D6wz-!A-%>T!fhLk4o||7x>VauJ}`SJ4BTu!97}n z>He7KEBq$@f?>iXdX`uEL?niVViT@`7u~EkAFd#x=B535rR20lE2apa zzqBP6DD>e9)h)Ot$dt#<!-J~9iw z;k_05kpO{emm4d1y0|3JTqiGn!){vB>+M;ui$u6eIDV$at_jTq zamFcnXBlAvUX2(Ve{nl|)oApX^Q5UW70KlT(Lto`MKZ%jQ7i!Ce zQD{2p#H1R4@BJwE@RdW_+V@L4xZ6}e8_AOn3iZcYVXuP8r}S2b`{|RX4HNjgz7NOI zf$Y3L7D1#qA10AM8{@n%pKJ@iC4+NP0k$jP6>Ki>Dy+}|wcruc(iRCExP;~cmcMo7 zAA!%ZpIGTxhA4l&h_M;|QS!Qnp+9}}*jPeOj%VExc3p}EODtDJT8~Bwp+9hw$Gu0O*$`YJPG%Hvw{+2Mvv>vPsPAVzolCdI}F zJg5rZvN$t%SsUj2F{FDmN{gLYQ zlJY8*(o=de<@Nc@A%I+h9-|n~g$eDC4LiF`0O@lO+~nbeJYcfHs3QV59B?RH1DB zDrI+3qDi5@--J-;<=yIO2*7);Q7< zv3MqlxMJGs5>k3qYDxxb93Oy{_bw@=*_lBj#aFu_rn4Mh}{ll)|I|yRNp>Sasv& zr4S)d%vij!<4`!x6CSkoh2t1fkqlR^r1{W%_zbuSw~}AUCX6(-j})&dvr-in-JnJp zOH?&falh?KS6)AUG`Wd@_-Uf3^R2@m%cNFSN|E(jWl%acMT^9brPSf%(wI+yGW#LJ zYfq8W5G1>Xq800f|FHMJbz8D}QWlV{ii??K=L9+iZTh)WkAM(?NJ6m(;b>rEqwEa0 zI|CaE{Ne}=_v%KAg8foVM|eb{tRloV|E}IY{HxycBY;b5s~q=BEZzQ9@9*`8zsI!X zu-V%U&7?*p#CND5+{Gw5s=;v^Z5z zBqQnA4&d|h;{NIwN`C_4ONdEXQAL2JSIC{w8x-l!JW?aBKrj~bcVyA*nC2vBqN(CNxHvFW{EjDPL&3bHVTE%@>!L_h#-(FO@6^n&*fFYpvLFS`F=1K*<2hLlq1_pf_T zOb1ca6PZ9u`d%lJ3qCHivSaLD_r68Jjs8iwm}@0l5>1?Yl07nPU*a+G%%Ess{dHde z-Vuosi}ueIoM_@Ysf{W>Jmrz_Vn*CxKhI=vUh@HxjGmb@W3_WROVv4%rk3cs%0FCv z!8=#q_%|h&*Z5*Fm`<2063Fk&Jy-(PDzbp;ClcL!-Pe?;g=ZBAT*RPnuPGl1PKer- zZ8m|m*wva1Rd7?*`%umxC5?kVC5#LkIIT&q$cdTE`DqMt0xNH4R!e7w!LKZHj~C~a zbAbEdVUJ_cCIN{b3R_QY^is<`A;X??DmuH4z_w#qs zlwi2Jpprvv)2rG+LV)RyKGbuJ)fL!L2or5&S&`biyismoQX81ec+o{Fmu zV2@~E_CwN+aKMnoHxgP@%>zSNynT=}%?Jr#pa}SA0xP-BU>c)Zoqd`_xH6zB2xR~; z9^5md#_kB@(tcl4&jB=V0r$7}E{y|49Cz3C9pnBS!gYN{Fi7K{y}ja2f=X5y3TP*q`RX}I7h!QQ2DdbMY-3VMH39((ZxoyYcY+_V!k zNT!t3gYj$D&;D*dVD4#0A^Yx$1}V$~fAbcw!Jmi`8=g0ylKMeL7vA=K=*-;Os9z9k z#9!&-)4;9tCa>I=dmjZLq8EUJOrZ7!s8E*K8joY%vi%u;(aYh zyQvBoz1@ggCA~>T0s}*4Kvgy{wcXJmF5C%PkmspIXbI5Hs5UWc2Iruv9gWzft#$k! z2TniyGrj1mfcMqD@zHQFWbQeMYUTz=)UK3xeV;6gAI35P8ft=Uuojdt7pQND6(DI2 zYs%v=pnkb=J^-D8K?3UIXFTS$I?f$p)a6*(IyU8i+%AM(G4SH{*7O28o3v~8OZ(>! zbX;*fcH;71sDA9X9s+`a|5?{NRL?#El+|@H8$)0pf~r~#dq5j2b^k1PdWBEJ|Fs&Plm?1*K%%86rQ5zKEr92>*%A3Q>A@5O>3M9t*b7K5b+v zfBt_wy=7D!P207NOVHphfdIinaCevB?jGEof#B}$?m-eH*x(S{-QC?~`s2EvcYS|* z^{S~_-BVTPuHE~%=OdF#HSww*_NrFf<7YNZ84rW-m9TSGjUvNbb#-J+xR7C}i^G~w z7+miR@FDLQ?t*RATZyCR8(?;zIRF>CZf^^T{8XGy9P&hrCT(&;x=bX{DET2rz zEh&YD`LrDmWGfj(yVW2AHc1(8agSQkdFVp|LGL>sn**Wy)2CW@t9c-6>o@PK*RlA{ zDB;aWAAST&UUG;ULB0|7AVdHm(kab{-sMarW2sR$6<&&C)+3jFb7UBv z06H0dSRMbGfrUqHgG zggPj@I_y4d%b^k=1{+Fdh=#-o6(H_}JH5rE?&!L9r2QYNzEY;qfAdj!R3YO)IEes* z03TIn=ri$W)NWR+eYtz3ur*}7AqU0}S~!76W=>s3*^ztmgAA!=j2)J2#v3%cVF!;F zV;3i_7=jUfdnnI&2zWnpeYn9yj1A<|ZPZmxRM zeV)@7=C1&VkaGgR)v~l84Vs|K`;hppCvg1*6B_VG95P|Qu@6{%_eOTIr{YvXD6PseWxeiP+54h5X@eQ?R|#JE~sJ)tYD*cywSb%dz z-EwDxSvauA682hTb@h~|^9ri983Z)Si-9gPs(vZsIwC-}TpL2}K+xm@@mSag9K!Z@ z0caYoVMvgu@mY6dVgaPA`+5Y3x_$pkzaK8Gtkmy-7}R8%qkJ`9I@7*P@OS^p9>S*N z)BMVu=hcTT6&Y-&XOIK(_Xtp`+-pPi(4ktkE74z2GmEI&32s^c*vHng5Du=bRU*_% z?UTuM3`$w+;_#kyDej$)7=npIv$k<&3?9UqiwBu2Zpx|eK454b&>rLg1I(Wqk4O+> zNg}A_mzoAIn%xqNDOTH(pQ!MV|9Mh3T){zTu|En;oxAGTs=MctUV04WNITRkTIF%| z6lxDw9PJHnm0e65+{Cw`3vS8X3&&uzh&hQr@OzmAet%ixMF`uP{bEk6Sb5IzH7q4n_lhr{R`b)ZTR6gHNTB{W4ijNW)+_?ukS%?1F;XZoqZfYyPGX_qe z=#?(npS7pUjN;rD8yEWwvN~N=-}OaI=jAvR5j$Oo2IwK#P-5!J$8?BfbCf2R zKXgPxkDJtJY~$8mE*mcm*vDt}9d6R0tng%cCHL+Bl@ZVnuHW^E1^W{Yh?884KU1xO zdmTB;UoVf@OQy;BFAyTjEh^qR&IW4+*UmSZ^(;q1oU3QNmiWCEg$CAdlmqDh!j2dwd)rw8%5bdle6B3nuAR zdSy3zzJaqQWBay8p(nfYXnLxud1Ow|BqAE}Fyqmx#AR<$39A{?y3Y&~*iCZQVhrnk ztSA%N)M|Cob=|`tP>Hx)nI+4luiNX5e!SA1|J?lLDR{Q(w>h2EE(VHSz;htq1CWtXPWsM>Z&8-Wv=c8MEi)rB3a0?9Y- z_77bV54r{1j-481gPP0b-}(ztZy0~Oid;h$!K2Bzujq%oH|Fa~WnSVbat|t~Skw4j zDiT;J43g%FD)8O=1=_4^Kd%^D<!zT=`ohhWE!Y=wivoGNx?M(_Q3XUS_-+qSiSHtl=U0yQ` zeW#!Eo6z)TxF(&KRSL9r+_Y1Q+;XCEGg`OB2r3RlE36ez+4k&WmZUO=?F^Tut{g|f z^}w&ePbHoe`=AMT#8?Qs`u05%7KJS~gC@ubo7hum(TuBX;X&XHJbo($j^BO}d`H&9 zdC6WbvqzaGOolQo+rJrLRWntpHAlK@Ty+kKBMD)!4 zAvBigPG!%Y6%8l3aw(-~-!4ndW-HI9kUP`U?RD$$w8f<_tg}c;sk&@+JqA=}w7=?d zR9%IOA3=+vn6@=QaA<<5>Phuu^{JbMo$@4$Ll{6eR7bMI}|tHGH^7j;eWim~OS z6CjNXVctNvh~~2mm_a|LP5*RLX3p`TSET! zy^Pds17^@u9zY*xvd)0y;GT1!nvU>T$PoQWVM^tSoD5^59VvGDOc9=Ty=BecB6P5~apUxuXy z^Av?vNxrR8Zw$bi9BYznp62)qhPHI0I&2dNP|~*C!`(P4X0gzZ!^E8;^2=v|rxa#F zFvjiu0BkkNlBBwCT%9f%Z3UgHha57Q5MqCiqSCFKJ&EuX!}z*cdX8?ueg;@+JbACM z&@S5&0DNu0q5mHaU`n0K0t_0E0!3hl1(0rmHjC1g_StL2Wkhh8sA7@1g-0q@tS;+O z>2;Um!O_Xw7i_9XxpZkla$AS0p9DSmAp0%8Maho`k-FafZBuc5l#*e&^|cA>fT}X9 zmj=!F`YmMr!jM1&#qzAI3I8c=DXV&`dHRjW7%{rB~#xLqtL+O$L>ZCy1n?M_|^4{88Y%t{_i*%6sg{H{}~Q9;k&Xj zn$=+SCw-B?#UQC)pL1K@>=pd`xUGcu#1y82Ef`*03W6*Wg2Jsg2GNf3TSQv1FHMlS z%3E47H0KPHTL|PX&UVvE6__J+0}3=HN}tGcD0FG=zD;f4ZTxtZ^6=Q$2mJ`KxEA`9 zgr+K-$gf52xc5&Upd}ed`yL=fktz+>CJi_>XzBxvSB7Wtf??Gu>#8;)o;3J#uh34o z{9os8X8(59>(K`Uo z#YG?va`B$7@68D*!mS(xC65QHA3#&X49trlM>nFgWM=k*logi;5-6hXz>&b}#?jIf zh`|gGD0y6`gOiVnR0q-&ES?RAjaOA5VAB8eRM}wzlZJ%Sw2+?4r}ynOpcM9_n-tJlmdvzeI z6D!ZDVv~qaHCX#|^wtm!ExqE(Ya8_Ak)`eaRfY!Cc>fJX>RI%R<;+%duc7UFrH;llQfL|5> zCe^71GEdNEh0?IA-dOTbCqOS-4gUa;PBGBY@#e1%w86ehXQY{9K;`4BAY#_wpPP!G z^A`1&9E>&DRz`xse?5GALi%iYdRz|szUry+Xw?(>rVHCX743fPClKLKMe<3>Y?NddzNR*x%hBKK8lW{g;I9HC07foD50XRmF~(ZQ0*u^2|VKX%q1T zMra)k!R!rxcYG-Fd_B3`2>TqYZxIqk&@gQ%M#!gB(z2GFwO*$a6WhY8CIuMSpEEM^ z59Tx;p)L(RRSWmqEubfPfw_j+v7`(cKb@4o(w%*Cxa^li79dthtNd<{KLpv}M!^%| z`sem|M-U<6#P-_^Dr#wUU=O_y6Sk8<3yL9IhJ? zXGb4Zjj)NZ8Ts6Id`CGpVRj0cZI&s|=Czc^>z8E_d>~;2)77WSa;d3*U)2~25KXN& zB342B0&_Q+_1pcRJ?o=PUdKkkLo(9#uYjzWZOjr3XUH_zD-eQ(K zbR)O0lwW-xn4!AbEgYru)XYB4Y-xSK`bhRngt48fq-tb+$lo9QnbV|8yl&GxTSH@0 zjv%Ek{ExVGsWx1s1@bfXL`&T5+8@&OU|j3?2;y%^qA^B`876Fz{a@FodSnvCxc0M% zxE9~OQ?@BH!wU%Zhc{-`dPeit6U%cF^1cmy-h+nmfbG;K-iLhEImWWjzi1(`l{_Ay z$uef&q~5l;Xz}&?shs0d@r3D@j&w}OX=af#G8OG@{dW-+sUDGqb?&l%9u$%z^2+rS=Sf2nBeeMT4R_H2 zOAq4t^=H9D<=-^{!iIYvqPD}g`SJ#+#=<`h^r#X@4t$lpPL*~Il{NXHMd#}uBLH_v z*|CqGU>tB*BmCVI3?)jVkC`eSiu(!9iz;CB+hJN)*|$TEMm!Z80Ku4dfLlg4kEij{ zCJ~u2cXDLnM)xZzwHGIr3|(HsJ3V;cka|fzx-KWmE8EN&rZ=LQgU9ZlhP=X}SE}Um z7uBb5gLL}JOSTTHWUWRq4LSUhR`mV`;&ifcP~C| z;U<)Iwjij|7|E+339+1|aK7m@D(y{}LNi;Ry(wkgDMe2yWlgO3miQ&! z<=F8ChmOsSjt#zAu_P$dv#Xt(kUK;8VX?pm)gdCMxi6r0v0F3SDCn*o$10JRWxgRK zDU5;Itm;?W&#vej6~rXM_Af3vLji0VVuZ*J9${!B#k4seWjAe)963*6a$PT(3J#{b zgI}?)A+FZ}2-AdNaJ+J1<8@3v!|w!j#9Zg+cS?O(_MWbYp_-bZH%Pocn8cGfwL**g ztkYoeQKbJM^;z$bdSvZk*Gmb?lMRx^@VE8Jo@#O@9=rw|Qxw9#wr*wD0-`3Y;l@SO z$OO`5g8Zm1ukT;n(!)btM#5U2P49#W$SjEKggz_4_z8naDq^*TK_P~D4L2P7m=TLr`mqbTmli=V zO@DE?$Fa9aNLVvTIf%E-d%+L#dii1>w-uR<0~YZ`+gUeR^oMV9Fy6XZ=322S>hUk> z>8z;(6Wr&+_!t>u;PsMe7+p0BP0$tDXoO3K6v@KBj{Sy~kqVkL)Z5Z7fq{?(nxO-M zU9AKVFL*Lg+`EMr*4W)C%0pOFd7}+>uY-Qv2Yxzl z16seIR{-<&;lJyO7@5ECi8kLl{J}354{!=#p|ZLs;x7fzk0<8H+|ZRu=P-SDgD}r^ zIZx=DBaW^u^;=Wq2x~zYhQh#R9m4LQKH6qCOK?}kGm%i^pJH%t=_PVu98RJZP7>Y( zuOrh+(^ zP>5k;gW*PwK-ectyPkuXpXU`!$B`Ds8aNjB4(q|s--VgzMz7Iy!&gupCQF+=TpefM z9G&4A!qZ+F0DMeDIAudvK|S!fU{S%rkH>1|+f_UP2b?=hQ>|G(o$vS8u3~9ueLtsN zbcs0R9$re<-XK5OX3qUVB5f~kF1p8m7@Wz|@mA)*L?K0iOeAPTA$h90k34obifp>v zB2F+9y+jkK1Zvi9xpJAsMPunsUKM&CVnj7 ztJX_g6n4Ms-TI;wH6y{>TZ6)wQ~&ET-ypTVmC`Q?7c-cNa2UT9Y~C=Xs&1G|65NN^Jy-he zLM2LsKD6yetpsi0Ewwa#QYjXozAe(*5DGjD3asDIGX%evGM?1_t5)hgCMeU2_;Dj< zbX7H(6v1B-d5pGj%tWXuI;xj2lgsq|n)_#de3NDxO|l)yJri@GNDlNoMEP4YB@uS% z`3rN$&LQ9+;J&ysyh_?^2I9?USqGaBY~+iTE#h1#=aC??o-{bd@8||hlun9=hgE6h zrWiPpnZ3m7pKdSOduq7mY#Yek{OIkPr)=n`p7ci=i)hEu$t`tJTyAYoOhAg_PGFsF zDP5@R`+Aj3NxFG-d~Ew-z|8w?Q?70b#br(vfp3AdAn`}KgS)$vGh}P-C|}TE z98kD@xO)NwmTm5U`9%6-!OS7KqbCA?D_mbPKb>Vj39FE%Yk6DfUiUpyySO!sTw&K- z;FTcNN;6O@JM|CHyNXB*weA2MjK8yNJLcaZ@G5fU1ooKyuZ5~-;Onm8 z+*{mUriQ^|R>|ib;3_Mu4ba|xdH)oxsCnTT(3h(m0Cu=W-~m_*tAi`9M2$;8Xw$jT zZ=>|(3?M!L5MOHEE_9~fAVMh9EEkW4F0yTs{6GI3zinuS1OBd#i|qiP?Zf+UTHin+ z<3b6P*4CymI$YXV67$PKNajRWUWB-vRcHF@667|DI`6MR*Y8XLGbMXh^Vp>bdo|sR zYj#}z-nclheX^}|l>3$lG3luV4DYUu%|NVj{7b?gMm|Q8L_FpA{2vs@DawM@&cn;>jLG9QBicu3Ys^T7f)-y*!)D*?if*Bw>%L0XQl0S(%Xztm=L$Dv9% ze0)C_mKX9`_AG~Wat@@HUx$;T_fRgW(A&f?X(uu@h+U>n{U4^DRb1?MNU++?@YdYi zA^)^u4_Rk6KHh^at;5E}sR?NJ7l#*qAlHYm6?Ges>8btbQl8p-+ID%(3DD70ldqWk zOKDkIiMYgB!kom9j>|v#}2t^Qbw~q$Pe1=#Yi#42Hx*fRVe8;(|3050E z6(N^@7TkBhoLQ+EZ$AE6qLv2EM*pD!87bZnqawbp zS6(d_EUP+W(rTW5_Qia3&K6k4>L8ATaD=S+Zah}R?h0Q2InqaUx7SL(%c|o?b&pNW zxvQ%4uG-2S&9y*7^}*U!Sa4-5w;SU2YzsQ4dwbLOknih_aVIsp_y{sbK|zCXa{KGe zhh#KrhmDf?k}i$rZR7n(+!_7cdGtLJ66j8+P=Dh4>d7hk;nca=aCYbaL)1I|hp6xO zE0j?9)#ZL%2!B-C%P;8evs~d?rW($!v(SO?Fmhpeq5by^_gsp%Wrrz1iZIWz?!!ix zcUAMc&x-4@a2rBL+D}B)Ko#h+w#)C|2(;g@==7ITWQDc=t=ie<2?&Bqa{7oW|ogqJURkj zXXJ7WQ+6gKoeFaT9e0&Zr6{F!2Y!JuJw#^O;^~uWkWsTpiV1EaSeaiVjH8VQeht(K zxR+$^AobE+dP%~3d5-TbH9_7Ek(vQ*AfIKfvE|Y6^2=e|L>GPU5S9 zcsE*{^N)>Z&V$ZjV@$SSp~Y73ZM3U6CvtZrIHtecS)oqc zV`a-6d{6CR8t4!AZr%MK2T%xNYHv9nD8TW)9~QHIu$Uk@tf|i(liEnu4gK-Y`2czu z`eQDQavJS+%8ql$M;55aJb^M>IKqy~4Xt&xxHQ=mM6qu%(ef}c*-P(kHRzn!qnt{H z?*pG&m%y)4Fn{0RN0@*`;MEPDo$amqYx50Lp_lx{XDz6Ml!74fNGe}Bhs(R_a)1JP z-3 zDx9Cbi>x&p`=(sW>&1!u$EauENuC>POAckF7cCd%{P<<)R*RT;yIE1}^XB#=mBOvkM%?Kjm{aC(4;^ASG)`_e3>W4p>NOMy{^{=1`tfCw4kS^1iioaEBnl8 zevc|f(X_@Bv@DjTqbRh7ZOkcW3)wAt+Q;={YW0ZDLn7diTNC&Z-}tWzltV`pC`m)b zRkI|Dlc~Y}DD#w}exI*B=x;YArG8iPtlVQ_a8BEwsqCv{ueOAeFa>JZazwKr6SdEQ zr+6UT)rgO2j6=88m^y0kCWQ`oC)Af4b$=%`^#LaBtl(cJC`h11$sIrVUc6ldo&b#= zv+u^}+RsduK!psr9lG2&xOb>QBo21vUr z9)LK6xBo;kS}N4XCNtJ~m%{T)5cvW+9$uT9HztAQck^&#@pXHwl^)rg-=M22lZ<3* zbYZ7Ug&SK7bGIcseJRQmh`ZKeKXVgvv_nwT~R+4dhm1`G#3ik{vV`1TK_0)#O2Qy1uZGv?;iP}L%h%| zD-*cVHb(m=_I_eN-g%D8Hqvtc(%R{$aU&#p)a_+;t9YWKnp?#wu{8z(Jx=)8nGck( z1t)bsNcqXoa|W=hT#8_iiEO{Gk-8LZSdpASeW>2Mlv0jSw6GhzODD!#iDHb;Q1+kv ze~kJc?~M9#oOedOne6`<^>YL7jQWm|&c9;+G3qhV(?qy&Y+d5L;Y}E@zVR z6JF+gKa^)coo(7(zM?;V>b*3;M;=IE&c?r?>-qhvRp9oF(bhlW|2()R`O6t#A-6(! zgnD@g)h*wF(Ei;4FU7`~6lskp)vPiM9|TkXn7v{+LpsMFL5OD$?>lP_2;xVtznA2h zxeNKpidiFf5r&G*`aV#~Cke9ugOJj1V zhY3LPwN?XoKTQz;=EMykw+pNie0Sv5Ix3K~Db04^jFf#FbhrEn{;wg!51+TQ9HnEs z(mN0ncPfO+hvB z0=bTd!OPW!-^Rl||6}ut(BY;ZI?r$ORoWVt1{y^u)|r7k&#~_jR$4w7>v(EEiFo}S zw~6d+eJ#KBB&+kibLvyxIrUG5A<|bG19_WP|K0?7^*9JnO#P5UNqcD&?y2h@7)Bn+rC6Bw^Gm zFAfj&4B{Au)0{6~_*4CMM^yC7^xi(aSy)q);O*H)>(e`-GOhFrp%a!GSTt|Y40^zk z*2~&tG=;k|8jBv&EFv*xIcHSpEOh^*RQ(m~$MKfu_wb6y8Ss8;!gS;hI8@QW%{>*T zKp+t7zfXCCl-2inJ)0tife#x1`;O3^&Rbq%@vtjPc>Amxi*{koejGQG7o_U!Gr1>% zEAZ-Y2ng9c0C&a5Pqj4H&X(X%%H_Yn&v;UguO`K>nS9`fJsys;ENe|R?Rqdsf6 z)S6B_0Pp|UG^Xu(7zH3AVnEdbpX~%_Au;=A0j1qTw`WMPs{CM5NJ&H2j~scNzIDC4JSo3mkDEFMVPrH6udd!N>N z$uuBmmeUN_T|DP)OUC;7bAUp~`pg}TmimnjhEf>d>U#NXRgGR${_Qy}&(+aceHM^G zsZM&?Fa)()7Ek?xVrpB@He}ayvna{P&B)Eog|HpRc0P)1+S@a!mTAiN+Y7^Hl)JgR z9@+x4KRlWt-zyJ2XMKd61RAHT8;zsRD$u@}@Dqi(R<3TseHp_FZPqLL2bxJ;I-RZY zz3g3cyBHK?>|RV~9UAGKg;VkMd8|j8Dg52uIv?T>>FbG7(~%#?T>Sy9%%3rF$@;~Q=?JMK_3rp077;siUwVo=A# zxUhLzA87H?H?)?BUkBN%10OqsU+yo_!31#$VGNQ%P_wbPx)7MhW!e~(1s=96GUZQl z90bZ3?V&Hn5yyfaS$#hnwJk8ud3-#HT2yq=jEDSAazX(Uj*dWKnoN`~bSYv*)9o0* zC^)CsdUfVtxR`q7RRE*V zHXq@@Y4dp|49e#b@?iFEjed#i$9XX+>lnKG<(WEExP`D`RFMUkW10_xlJCIdPePP{ zhdU7fh(7MXn)$kp{Bk-nIn1hyS`a|ypQ%ZYN%q>xY?JD)QLzQ52q!}AvHN?scwC;>x#hQZR;}!z)=S7g z{kr`CaTyN{3e1Y;u5@$l^|Q9Mi!t7$k^RM8S1-J(pQuIVbw)f_GvK%mXLAh}6Y6!n zZ2Vj{6OA;^O4Ikn&vBo!WOXN24orKt17=Ak|zv8{Ntt1Sy9fP7w! z*xb*sgAh1ilUgAXeOvS~VKBv6Z$eBDl>(1EP;Vzpka(WwUNjMHO?cZ$j#t$!>+|^< z&QG}Fz}CAqXv_4NBKH1`6L$2U_@Bu>sbr0ZX$QkEP*#6dX{l~FL3^E~HKX3I8DQyF zd-vdhkh25VMYF?;qHmf8pjApjXQe93q0w^1q8#s++smd8S^O$fZ<(cr?1&xbi#Q!mkuj7#-ftPiukDanh6)<`Hfy2^D6j@34x8p?qYInm>vZN zta}h~pYp<%13xk~Xo68klmRuXcZ5Qj^<#9J)DNjby?)pfq$eXa!Jg%l`Y>6ooY;h! zBDD3C`ujtq!XJi6g~YLJp#0JF2ZqvWnPG$HvAgLW`)hZ)njIPsGo%Sx)=WRXuZ|1E z2J|fhj$+NVr!nW;ZxqMV7KFVT+xH&_Bkl!_yY>qh3O@PE47ipJvKKBuJ*RP#$pF=S>G&EF8eAN?R|CXzniB&5s4787I?e2Gbl2WA-YyOojb&ZJ=>>O z=!Q{e8#PI84^Y7q(M*!JEq&%<#2e9W!WcByma8V82K5o* z4SK)=QKPsEQ@cZ64@aj(H)Uc6r#oRYy|4$pK9&|<;N^^WDJ6*Zd%C-sH(ZO^O}iQ;3XZxSsk?QXXpu@x7vktt&sbK?gUnj&3zzKy zXX<^hzK`K%kf>HF;OOwy?P#PR6c=L|iURF9#8#v$aP7-)#{kGnXMegx6yz)&;lp8g zq}oYRUGi+#hJoWx%J~)Mc=W)skkOV^uUs~|)=Q>T`-4r<66WpcxHC^nn`kfQMDCR@ z-bxzAGuGCH7poel{?0ov?nv^J#!!*TF=s2-O1Em=bN~Pe5J;77Kw?Y6{m{pl#fVdYYTexrvz2?*rB=VubRuumCUg zLV+oV`IAcHbB)`<{{Dc8d!KzT2m8tohh_u&Mqb^GMqZYt!>j&&9l76xF3SN*hI+e| zNqGXMgp?-cl=(v6%*MV<=1@8)RTri&otKQziQdW;vhi*-7-M}<(4*rDY6!gvE{-PC zqWWrsNma8U{I`H^G9B(3;i#X@=3jMs=|5BI{^*e}0t)WvRy{Vd zUe>aQ0@*O`6{Mwx2$-brGB>t@PuF4?L({a`_+P0o&%A;aOrW$27@SgPjPws)CO(mv zpw{mBhDcXp`87?y&Lo=`h{Nj~MD^u<$6_EvVU(MRr}+I*$%HBd_4qHoz@V(CblJzf zAWMx{jy4bKRnk^UyR59zQlS}Qt$b&uT`s&s`zuk*P=ZRx|?w3C4!I(9kn?FXX zB)MdcBvDXt-RL+nYxz0V%_0#At;$QQ9>Fs(Ghffxj zIUDk|t}xm|SU@;R8MPXet?>$$Dt`Tnf@AAvY(7rHuJu$=G$T8N%zEO%z*I?TMJFH4 z>f_zivb)booY7CqU8I4lgkA6#q|=sG(&-&*kiDaBgHqXt89% zd(R~)(YC2&l5EzyVIAFFG;BtV^6Ep8HLzlXB_;`M$0v23Ary!$Pu@onn8L^HTj8yi zMcrS&Bp*#%jHZOZuHq%_~YS)iZasUW*|BGr;3%q!`I-#iG< z*jU9qhR~&C`$=z)u}kPDS6MrcXntOqwZneoqKRywaUQh30)@| zjj5kIq?m5P=DBuGq@WnB5tvw5d1Q|IbmdQwl%aXm^cxY~X<`JZ6fn%V7l{-U8gXr zOy4@~*$_2{v4$W*lA+J$7iMA$dia8R4J*h_B{2d!l+G>74$`3bzVKqTdQ!cOP z39#)X`w{DPav0eEO3t($P7Zcw93He6_>wquvKW{sUzAGXMC3A}RO^v95OB@zn2VlM zmT@z4J+oV8{%%E1{vKus@T;=|s)M%#U`hpt(>vrq&p*{H_9_?pP2ww)mOuG$#eWHI zUVfF>D@3u}X}r0`F66oia%MVTOo>e+h4c!>A&=c#?>va9P|J`|NeW*tep~l9@SUUm zSR)rkDAUmn?-X6qhdAt?+w&{6=~R)ssA7ZRH1~Ts+b3U63&n8E&p{`}NYF5?gZDgN znv%yF;k#-{ho8}bTGX<-)bU-@U8VM|g^rUK2Nz&R!pEkp<1Wkn;PVEso>8%oxhJ#= z=(@Z$>a_(Hud8k#dv*cfjt&9Ho6^DD$UX!R9oB+iFtQ+1G>M*OpY#iDtgZV~Y9~{! zUUYAKFa~Y*96d{Epa?6-sqG2t7A!?(q5NT`WpNVS6xIKnlE5(_WmOI38aE!f;MO=# zUy=fyjH~IbQqy9h(WnNYRr=s$(~vuqRIWwR$sf(d8YZ7j)l~iuQKjB^p)n%U6{|Zb zn{hJT^&Asp-gANbiU9hAk*$QYvZekX)^-Ejs8wW-QJ*`F+&`8h1Aa5NlTWoMa`sSa zcqMoF;x6kTA9g{Wrm`u=M<|If)ZUJvM}!EMqn>aD@4xba14$FnZxSQ#fx#J?EU!juT~i7yR%3g300mgB&Lh3v2|3V$majB-(4 zU-$>GiCAx!~T|YEzAA`1!P`F*(vfUMG8o$#TAFAy%u3jEfgi(rNSWI--b!3!W?ZPlI`u^Ie_lmmTC&=mvg>a03wacn4egwE zwH&kGQM-X3pIGtk# zNh7n+x@pi{vsSXkI0_3zqtpz{8l3uT?Ukd#H;k2RltPEDaf_Ka{(fi`;H&nmijBda zjpuV%P|D{>0_#aO=TTRS0k`KI4UVRF!>#0`MW}RIq zr5^grkDnU(CsDC{jv7y&vpTGt7*(`qF!L+U6C1iUgemMMG9Qo;Fxm+DESa}F<~(Rs z*g@C9`jhfw-Z8QxKdDOkj~NW76KK<72tX=Jk~2+&k@-w-jTa zX%VgG9ILI>{c>s~J2_jj$_|WO6|;xlu}Oq0!Fn^xEic`(cl|0U*Kf*772KOXS^W7^ zkJna6{+a0T{r*;~_hj8y*CP@0vwt{_m(hGDss}7XI}N!p{4#VQvIu9c%eaMcO(-jT zh!0_f#+9^r`?Mc*zSx@whYxv)vER~i-T7!yAzb%8rMDOp5jao3-mui<+DrRL9+5&gymm(^)A(5b%r()TbB0#I&13ri;FLR& zixK+GkV12dDwnLI_?__Xw_4napdY-3u>z_!Y!CY=ml%?bzB{u{8v2yo2v0GYWH4BM zm3%)$c*pLYwGD-Wu=(re=Il(!z!ma*zGDd1^IP}PFoIm-8PH#f@3wp^mojy$(%0om zuv4mS|I`$vF0dga$mR6_IEx8C4XIi<1wENZ7ieQWE_FF2yFDgr8HJhhtBA6drOceJx&vr#VdiIEK0f+iyGDK}YS=qG>IKJ1 z<4TVV1-fF0Ktj~-P5%By+@Ds|%89mX;BujMAIo|W@l^M@88^4lkOSP~{l}uWBgrnHDU>cVT z&;kbHmG2?FYFp0fluwlj+3~nIzXS|Hq=igM`}c$nk4R}zR`5lowzu0{v_@tW zoDaq6JU$qn{hx6MoId0(g9x`)A5EB6>lD{myD;JDKDHB8CD&9 z_{lv!etG|fu+jf7wFOH01ffx-G9P5-Fhmq(X6%cU&W z%SKouXv>pXrpx26$fNzYt&Nv|Uv_@t)5OlDUB&2hwTIJe({LF2E4ekNZi@TUB%rz4 zMi2B@S-vZW*Lk(^9gVMQG<2+ezgfE1*CYw9C^h|m+K~TM{C_*`mlN?n=#R$#w}?{m z^gS03pOag|`AU>tds_E_^=c1szxY!19q;dU52Dh24nn-2lZ#M5-Qy&b3NyJ01w`3V z$j|O_72;(rXCW$1?Jktz%;YfCSbMC?P@2!hX(*s(bsGwl$2ktU?G&y<>8sLtXkOKK zA36faav&NQv%3%tq`RGnrp6=Ph^G1MjzkNmwOol#W!-wt#L2ePx)bx!-sw;@H6H0w zG|gvmD&{a|a4Xsr^f?i+j=Za-V~-Soz1g7%S#7wUg1nn8nR# zU_9E<$N(Pcd@^kx<9^E5`9XJMWzE_S$0~BT%jH->7E?PN4UC!Gjt0m~1W?cQ`0ro; zS`Nsn>a|^vISt(FgdD5&7&oK=@mNP>-hKvGq=7MmGtwZt(j8fTX*nb-1ghL6Y45B$ zPRV=&jJqX;)Zpgin$*zd8vaeY`ZHfkvoIKQp1~%%TfcIi_@~$FSEEUHIykFmj&9#U6&=)&+5F? zz@~Fw8ut1)2PRtAbzwSIxx)d;;7ZO+esZ5Xvo=)Qv1uP2)virbV-C*ETtb_ld((iN zlY?_&gwwb<3*xSJa@r(5yPLCM-E(ksTBq^MuFf3Re4L#cTs?PZc?l{Up4DscSeIwL zHK^(IH0Rzt+@2cNyd0nQlFsP*)SxDupJwIe;{G(iW_Ey90h+}HnzP&aI6*D$Hy1ak zf%kwT6xHr?h1w@k4VS20{Xy5L3~_4b=v_m;*F8Gs9c?_K|Bcq{{=WTs>4`*^uspzplO;8QZcJRMl*nbE*4bahwh3@8t*+vR z_KNLG-~$>8tpvmyAx@qOE(!^Nl^9W%;j`i|-4LZg4}ip!Pp8LpY#-&~3$z;?pkrZ& zv(f|rq%e2gn2=69UI{J}$I4BNSwqk5S;_&(c6DqD{upw_88CyL^wqKJGP6Pel|KKs zg5YvnK;l46#zY1a8f&#~sy(WV3;N|cft$ikmIA8`Em3`vjG)}M0O-#o0be6nRURyd zW3fp9I0VPJ7$zA4^_7N1if!Qce7T&pVy*VLPk-sb4}S0e+;?7I``-Vt@2ubYPJHTn z_m{r)KJ=abJKtG9^PS}@-+6uHJBM$4AN`5%6mI%P_|av$`(KaC2l3GPztfqx|J8Zf zma=@*MyL4^INu$YUH#iBss6Y7znFS`F5=%RmEnfM~ujoQw zYnXg74EaghGtntay#9tGLrt%57~u9uoC;;R~&P7oL}qQho;xT>b9LJ?*}n z^Txa59WGW>zKeWBXPtaSLI)fL6)*e>Kq|bRd`R=is(Kyxg9^BVcyP{PP$yqm-s{Pi zB$?nk!9~Rzk{O7#o~M_CUbo_P8dN-_g&$6?=)ZeYeM4o8|LYAm-##A9|JkwZzb`w& z|6euye-TArQrGLgt3#9{3rwric2?zb|JstS)*TX)*d?Xl9pS9A<7~C*f9>yfcRIV< zFPd%Fl%_PLDNSigQ<~D0rZlA~O=(I~n$nb}G^Hs`X-ZR?(v+q&r72BmN>iHBl%_PL YDNSigQ<~D0rhMh{f548beE?_+0L2q{f&c&j From 5f8964a6a854ba3f1afd002b3062f52d0eea2aee Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 14:48:49 -0700 Subject: [PATCH 159/192] Fix typo --- .../certbot_compatibility_test/configurators/apache/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 918db5f47..ed3d9d67a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -118,7 +118,7 @@ def _get_server_root(config): if os.path.isdir(os.path.join(config, name))] if len(subdirs) != 1: - errors.Error("Malformed configuration directiory {0}".format(config)) + errors.Error("Malformed configuration directory {0}".format(config)) return os.path.join(config, subdirs[0].rstrip()) From e826de5db70823594d2c9c286a953de5fc656c11 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:52:30 -0700 Subject: [PATCH 160/192] Don't change line endings on a tarball --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 5eee84cce..8c41b686e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ *.jpeg binary *.jpg binary *.png binary +*.gz binary From ce378cec216d73c8f20ee3d34981089a1d842829 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:53:12 -0700 Subject: [PATCH 161/192] Try updating tarball again --- .../testdata/configs.tar.gz | Bin 100286 -> 100288 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz index 05f7f4f9bc54975c0f575ebd80613c9e419dc3b2..9b819d0c7b2d8178d3aad1bb25aeb32a4efae76c 100644 GIT binary patch delta 31 lcmdnj&vu}njZMCrgJIK$jcg)vjJ%trCjhE{3hDp= delta 27 jcmX@m&$h3hjZMCrgJH+(jcg)vo2BLA7H+QE?{)$JjvETn From 092173c60892ee79d50fad19fc1ff6b05a27b852 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 6 Jun 2016 17:05:51 -0700 Subject: [PATCH 162/192] fix broken link in contributing.rst (#3130) --- docs/contributing.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 3318ec103..267d466e4 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -266,8 +266,7 @@ with the core upstream source code. An example is provided in it with any necessary API changes. .. _`setuptools entry points`: - https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins - + http://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _coding-style: From 8aa1d85991daa33dc22172f4535ed65606299a85 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 16:25:08 -0700 Subject: [PATCH 163/192] Move mageia bootstrap script --- .../pieces/bootstrappers/mageia_common.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bootstrap/_mageia_common.sh => letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh (100%) diff --git a/bootstrap/_mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh similarity index 100% rename from bootstrap/_mageia_common.sh rename to letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh From 1c363716a086ec4dfb3b0c3143e915a75efd106f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 16:33:04 -0700 Subject: [PATCH 164/192] Wrap mageia bootstrap script in bash function --- .../pieces/bootstrappers/mageia_common.sh | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) mode change 100755 => 100644 letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh old mode 100755 new mode 100644 index 9a4606c9d..d6651574a --- a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh @@ -1,24 +1,23 @@ -#!/bin/sh +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi -# Tested on mageia 5 x86_64 -if ! urpmi --force \ - python \ - libpython-devel \ - python-virtualenv -then - echo "Could not install Python dependencies. Aborting bootstrap!" - exit 1 -fi - -if ! urpmi --force \ - git \ - gcc \ - cdialog \ - python-augeas \ - libopenssl-devel \ - libffi-devel \ - rootcerts -then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 -fi + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} From e51c16d666655c2c2405ea604c5aee76bf5eb4ca Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 17:24:56 -0700 Subject: [PATCH 165/192] Update letsencrypt-auto changes for the new format --- letsencrypt-auto-source/letsencrypt-auto | 27 +++++++++++++++++++ .../letsencrypt-auto.template | 4 +++ 2 files changed, 31 insertions(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..eef0c957d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -458,12 +458,39 @@ BootstrapSmartOS() { pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' } +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..4d15d281d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -155,12 +155,16 @@ DeterminePythonVersion() { {{ bootstrappers/free_bsd.sh }} {{ bootstrappers/mac.sh }} {{ bootstrappers/smartos.sh }} +{{ bootstrappers/mageia_common.sh }} # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon From 96dd662e558da73d178dd4fef0f14a5f1dc67975 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 14:35:59 -0700 Subject: [PATCH 166/192] Delint certbot-compatibility-test --- .../certbot_compatibility_test/configurators/common.py | 1 - certbot-compatibility-test/certbot_compatibility_test/util.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py index 4592eca39..03128cc86 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py @@ -5,7 +5,6 @@ import shutil import tempfile from certbot import constants -from certbot_compatibility_test import errors from certbot_compatibility_test import util diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index 570bf1a9e..af951aa6a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -1,11 +1,9 @@ """Utility functions for Certbot plugin tests.""" import argparse import copy -import contextlib import os import re import shutil -import socket import tarfile from acme import jose From a0be028340acf1de55426a48ee4e89994f5a4166 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:49:08 -0700 Subject: [PATCH 167/192] Add _get_names_from_cert_or_req --- certbot/crypto_util.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 6b1b8426c..8640fec1e 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -296,6 +296,18 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): csr, OpenSSL.crypto.load_certificate_request, typ) +def _get_names_from_cert_or_req(cert_or_req, load_func, typ): + loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) + subject = loaded_cert_or_req.get_subject().CN + # pylint: disable=protected-access + sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req) + + if subject is None: + return sans + else: + return [subject] + [d for d in sans if d != subject] + + def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of domains from a CSR, including the CN if it is set. From ac581951b3926c590ce930230a315d31e91ed0b2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:50:34 -0700 Subject: [PATCH 168/192] Have get_names_from_csr use _get_names_from_cert_or_req --- certbot/crypto_util.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 8640fec1e..e8047f086 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -318,13 +318,8 @@ def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): :rtype: list """ - loaded_csr = _load_cert_or_req( + return _get_names_from_cert_or_req( csr, OpenSSL.crypto.load_certificate_request, typ) - # Use a set to avoid duplication with CN and Subject Alt Names - domains = set(d for d in (loaded_csr.get_subject().CN,) if d is not None) - # pylint: disable=protected-access - domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr)) - return list(domains) def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): From 753aea2f3f495eccb3f50cf977c3864dc56a55b6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:53:04 -0700 Subject: [PATCH 169/192] Add get_names_from_cert function --- certbot/crypto_util.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index e8047f086..f45645de1 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -308,6 +308,20 @@ def _get_names_from_cert_or_req(cert_or_req, load_func, typ): return [subject] + [d for d in sans if d != subject] +def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM): + """Get a list of domains from a cert, including the CN if it is set. + + :param str cert: Certificate (encoded). + :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` + + :returns: A list of domain names. + :rtype: list + + """ + return _get_names_from_cert_or_req( + csr, OpenSSL.crypto.load_certificate, typ) + + def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of domains from a CSR, including the CN if it is set. From 8db1b5627c02d4a02890075e1a3ca6f9a3a29d96 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:57:56 -0700 Subject: [PATCH 170/192] Add GetNamesFromCertTest --- certbot/tests/crypto_util_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index fa88e89e7..742d4ec8c 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -273,6 +273,25 @@ class GetSANsFromCSRTest(unittest.TestCase): [], self._call(test_util.load_vector('csr-nosans.pem'))) +class GetNamesFromCertTest(unittest.TestCase): + """Tests for certbot.crypto_util.get_names_from_cert.""" + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.crypto_util import get_names_from_cert + return get_names_from_cert(*args, **kwargs) + + def test_single(self): + self.assertEqual( + ['example.com'], + self._call(test_util.load_vector('cert.pem'))) + + def test_san(self): + self.assertEqual( + ['example.com', 'www.example.com'], + self._call(test_util.load_vector('cert-san.pem'))) + + class GetNamesFromCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_csr.""" @classmethod From 0a707b64ec0b9a0ee61fe56fc6bcf4b1b9525318 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:59:44 -0700 Subject: [PATCH 171/192] Use common_name instead of subject --- certbot/crypto_util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index f45645de1..1e831dd8f 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -298,14 +298,14 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): def _get_names_from_cert_or_req(cert_or_req, load_func, typ): loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) - subject = loaded_cert_or_req.get_subject().CN + common_name = loaded_cert_or_req.get_subject().CN # pylint: disable=protected-access sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req) - if subject is None: + if common_name is None: return sans else: - return [subject] + [d for d in sans if d != subject] + return [common_name] + [d for d in sans if d != common_name] def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM): From 2c803eff6ab161079128dca16f8efd8154049ac1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 17:01:54 -0700 Subject: [PATCH 172/192] Use get_names_from_cert in storage.py --- certbot/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/storage.py b/certbot/storage.py index b0c8245d3..60886e306 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -616,7 +616,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if target is None: raise errors.CertStorageError("could not find cert file") with open(target) as f: - return crypto_util.get_sans_from_cert(f.read()) + return crypto_util.get_names_from_cert(f.read()) def autodeployment_is_enabled(self): """Is automatic deployment enabled for this cert? From afd899886d255f4c15e89ab7940a561083a3d2c1 Mon Sep 17 00:00:00 2001 From: Willem Fibbe Date: Wed, 8 Jun 2016 16:26:56 +0200 Subject: [PATCH 173/192] Prevent bootstrap-issue on Debian systems with virtualenv package On Debian 7 (and probably relative distro's) `aptitude show virtualenv` exits with 0, since it is a virtual package. However, it doesn't have any installation candidates, so filter on this case before trying to install `virtualenv` to prevent installation-errors while bootstrapping. NB, to make this clear: (0)#: apt-cache show virtualenv N: Can't select versions from package 'virtualenv' as it is purely virtual N: No packages found (0)#: echo $? 0 Furthermore, --quiet=0 is necessary, to be able to grep through `apt-cache`'s output via a pipe. More details on http://unix.stackexchange.com/questions/201869/why-isnt-apt-cache-policy-output-piped/202041#202041. --- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..3ed48216d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index bfbcfa31d..8eb7e16ee 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -23,7 +23,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi From 02cdb5db0ef2e035a19b86ca0b462a0a38e50694 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 16:03:32 -0700 Subject: [PATCH 174/192] Add cert with not alphabetically first CN --- certbot/tests/testdata/cert-5sans.pem | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 certbot/tests/testdata/cert-5sans.pem diff --git a/certbot/tests/testdata/cert-5sans.pem b/certbot/tests/testdata/cert-5sans.pem new file mode 100644 index 000000000..5de7cc6cb --- /dev/null +++ b/certbot/tests/testdata/cert-5sans.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICkTCCAjugAwIBAgIJAJNbfABWQ8bbMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMScwJQYDVQQKDB5FbGVjdHJvbmljIEZyb250aWVyIEZvdW5kYXRpb24xFDAS +BgNVBAMMC2V4YW1wbGUuY29tMB4XDTE2MDYwOTIzMDEzNloXDTE2MDcwOTIzMDEz +NloweTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xJzAlBgNVBAoMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91 +bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANL +ADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE +30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4GlMIGiMB0GA1UdDgQWBBQmz8jt +S9eUsuQlA1gkjwTAdNWXijAfBgNVHSMEGDAWgBQmz8jtS9eUsuQlA1gkjwTAdNWX +ijAMBgNVHRMEBTADAQH/MFIGA1UdEQRLMEmCDWEuZXhhbXBsZS5jb22CDWIuZXhh +bXBsZS5jb22CDWMuZXhhbXBsZS5jb22CDWQuZXhhbXBsZS5jb22CC2V4YW1wbGUu +Y29tMA0GCSqGSIb3DQEBCwUAA0EAVXmZxB+IJdgFvY2InOYeytTD1QmouDZRtj/T +H/HIpSdsfO7qr4d/ZprI2IhLRxp2S4BiU5Qc5HUkeADcpNd06A== +-----END CERTIFICATE----- From 4f99cc7b2a2f1eb4eb1820e75d909551eea2d1c7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:43:05 -0700 Subject: [PATCH 175/192] Add _write_out_kind method --- certbot/tests/storage_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0c88d3d55..0579c9f1c 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -84,6 +84,16 @@ class BaseRenewableCertTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tempdir) + def _write_out_kind(self, kind, ver, value=None): + link = getattr(self.test_rc, kind) + if os.path.lexists(link): + os.unlink(link) + os.symlink(os.path.join(os.path.pardir, os.path.pardir, "archive", + "example.org", "{0}{1}.pem".format(kind, ver)), + link) + with open(link, "w") as f: + f.write(kind if value is None else value) + def _write_out_ex_kinds(self): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) From 562802bfd0443f14cd928ee41a4ae73b9453ce39 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:44:33 -0700 Subject: [PATCH 176/192] Refactor common symlink writing code --- certbot/tests/storage_test.py | 124 ++++++---------------------------- 1 file changed, 19 insertions(+), 105 deletions(-) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0579c9f1c..44b881fd9 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -96,16 +96,8 @@ class BaseRenewableCertTest(unittest.TestCase): def _write_out_ex_kinds(self): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}12.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}11.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 12) + self._write_out_kind(kind, 11) class RenewableCertTests(BaseRenewableCertTest): @@ -214,10 +206,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_current_target(self): # Relative path logic - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert17.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") + self._write_out_kind("cert", 17) self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), os.path.join(self.tempdir, "archive", "example.org", @@ -235,12 +224,8 @@ class RenewableCertTests(BaseRenewableCertTest): def test_current_version(self): for ver in (1, 5, 10, 20): - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert{0}.pem".format(ver)), - self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") - os.unlink(self.test_rc.cert) + self._write_out_kind("cert", ver) + os.unlink(self.test_rc.cert) os.symlink(os.path.join("..", "..", "archive", "example.org", "cert10.pem"), self.test_rc.cert) self.assertEqual(self.test_rc.current_version("cert"), 10) @@ -251,61 +236,30 @@ class RenewableCertTests(BaseRenewableCertTest): def test_latest_and_next_versions(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(self.test_rc.latest_common_version(), 5) self.assertEqual(self.test_rc.next_free_version(), 6) # Having one kind of file of a later version doesn't change the # result - os.unlink(self.test_rc.privkey) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "privkey7.pem"), self.test_rc.privkey) - with open(self.test_rc.privkey, "w") as f: - f.write("privkey") + self._write_out_kind("privkey", 7) self.assertEqual(self.test_rc.latest_common_version(), 5) # ... although it does change the next free version self.assertEqual(self.test_rc.next_free_version(), 8) # Nor does having three out of four change the result - os.unlink(self.test_rc.cert) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert7.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") - os.unlink(self.test_rc.fullchain) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "fullchain7.pem"), self.test_rc.fullchain) - with open(self.test_rc.fullchain, "w") as f: - f.write("fullchain") + self._write_out_kind("cert", 7) + self._write_out_kind("fullchain", 7) self.assertEqual(self.test_rc.latest_common_version(), 5) # If we have everything from a much later version, it does change # the result - ver = 17 for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 17) self.assertEqual(self.test_rc.latest_common_version(), 17) self.assertEqual(self.test_rc.next_free_version(), 18) def test_update_link_to(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) # pylint: disable=protected-access self.test_rc._update_link_to("cert", 3) @@ -322,10 +276,7 @@ class RenewableCertTests(BaseRenewableCertTest): "chain3000.pem") def test_version(self): - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") + self._write_out_kind("cert", 12) # TODO: We should probably test that the directory is still the # same, but it's tricky because we can get an absolute # path out when we put a relative path in. @@ -335,13 +286,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_update_all_links_to_success(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) self.assertEqual(self.test_rc.latest_common_version(), 5) for ver in xrange(1, 6): @@ -386,13 +331,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_has_pending_deployment(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) for ver in xrange(1, 6): self.test_rc.update_all_links_to(ver) @@ -405,21 +344,12 @@ class RenewableCertTests(BaseRenewableCertTest): def test_names(self): # Trying the current version - test_cert = test_util.load_vector("cert-san.pem") - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) + self._write_out_kind("cert", 12, test_util.load_vector("cert-san.pem")) self.assertEqual(self.test_rc.names(), ["example.com", "www.example.com"]) # Trying a non-current version - test_cert = test_util.load_vector("cert.pem") - os.unlink(self.test_rc.cert) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert15.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) + self._write_out_kind("cert", 15, test_util.load_vector("cert.pem")) self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) @@ -490,13 +420,7 @@ class RenewableCertTests(BaseRenewableCertTest): # No pending deployment for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertFalse(self.test_rc.should_autodeploy()) def test_autorenewal_is_enabled(self): @@ -517,11 +441,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(self.test_rc.should_autorenew()) self.test_rc.configuration["autorenew"] = "1" for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}12.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 12) # Mandatory renewal on the basis of OCSP revocation mock_ocsp.return_value = True self.assertTrue(self.test_rc.should_autorenew()) @@ -535,13 +455,7 @@ class RenewableCertTests(BaseRenewableCertTest): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.test_rc.update_all_links_to(3) self.assertEqual( 6, self.test_rc.save_successor(3, "new cert", None, From bb1d2c0a1faf7d58249e73fdbd6ea832335755b3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:49:44 -0700 Subject: [PATCH 177/192] Test common name is listed first in storage.py --- certbot/tests/storage_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 44b881fd9..0d907eca3 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -353,6 +353,13 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) + # Testing common name is listed first + self._write_out_kind( + "cert", 12, test_util.load_vector("cert-5sans.pem")) + self.assertEqual( + self.test_rc.names(12), + ["example.com"] + ["{0}.example.com".format(c) for c in "abcd"]) + # Trying missing cert os.unlink(self.test_rc.cert) self.assertRaises(errors.CertStorageError, self.test_rc.names) From 07cf34284eabf129ce0fae76d79939d38a8bbcdb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:55:46 -0700 Subject: [PATCH 178/192] Add test_common_name_sans_order --- certbot/tests/crypto_util_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 742d4ec8c..5a592bbb1 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -291,6 +291,13 @@ class GetNamesFromCertTest(unittest.TestCase): ['example.com', 'www.example.com'], self._call(test_util.load_vector('cert-san.pem'))) + def test_common_name_sans_order(self): + # Tests that the common name comes first + # followed by the SANS in alphabetical order + self.assertEqual( + ['example.com'] + ['{0}.example.com'.format(c) for c in 'abcd'], + self._call(test_util.load_vector('cert-5sans.pem'))) + class GetNamesFromCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_csr.""" From 6a53522a6cbda3819bba0fd93072c50ef8095869 Mon Sep 17 00:00:00 2001 From: Sergey Nuzdhin Date: Mon, 13 Jun 2016 14:43:47 +0200 Subject: [PATCH 179/192] Add additional warning with actual exception message during renewal Log and show warning with real exception message to make it more clear what exactly happened. Currently we show `config is broken` when in fact we have broken symlinks in live folder. --- certbot/renewal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/renewal.py b/certbot/renewal.py index d04e2d27c..95f64b94d 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -60,7 +60,8 @@ def _reconstitute(config, full_path): try: renewal_candidate = storage.RenewableCert( full_path, configuration.RenewerConfiguration(config)) - except (errors.CertStorageError, IOError): + except (errors.CertStorageError, IOError) as exc: + logger.warning(exc) logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None From f8a07a8f4673d0771c6dae034bb755b10ddb8c15 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 13 Jun 2016 11:53:32 -0700 Subject: [PATCH 180/192] Provide nonroot guidance when logging gets EACCES. --- certbot/main.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index fa14bbf99..f68373998 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1,6 +1,7 @@ """Certbot main entry point.""" from __future__ import print_function import atexit +import errno import functools import logging.handlers import os @@ -588,8 +589,16 @@ def renew(config, unused_plugins): def setup_log_file_handler(config, logfile, fmt): """Setup file debug logging.""" log_file_path = os.path.join(config.logs_dir, logfile) - handler = logging.handlers.RotatingFileHandler( - log_file_path, maxBytes=2 ** 20, backupCount=10) + try: + handler = logging.handlers.RotatingFileHandler( + log_file_path, maxBytes=2 ** 20, backupCount=10) + except IOError as e: + if e.errno == errno.EACCES: + msg = ("Access denied writing to {0}. To run as non-root, set " + + "--logs-dir, --config-dir, --work-dir to writable paths.") + raise errors.Error(msg.format(log_file_path)) + else: + raise # rotate on each invocation, rollover only possible when maxBytes # is nonzero and backupCount is nonzero, so we set maxBytes as big # as possible not to overrun in single CLI invocation (1MB). From c60c28514c6a8dde2f94c5d557cce7649b67a344 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 13:49:32 -0700 Subject: [PATCH 181/192] Add global DEFAULT variable --- certbot/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index cff111f42..e2475ec31 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -104,6 +104,11 @@ ZERO_ARG_ACTIONS = set(("store_const", "store_true", "store_false", "append_const", "count",)) +# Maps a config option to its default value. This is set during the +# parse_args method of HelpfulArgumentParser. +DEFAULTS = None + + # Maps a config option to a set of config options that may have modified it. # This dictionary is used recursively, so if A modifies B and B modifies C, # it is determined that C was modified by the user if A was modified. From 657c4e725992abba9e2c80a9cd7598d97b79b910 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 14:49:24 -0700 Subject: [PATCH 182/192] Set DEFAULTS during parse_args --- certbot/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index e2475ec31..3c65527e1 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1,6 +1,7 @@ """Certbot command line argument & config processing.""" from __future__ import print_function import argparse +import copy import glob import logging import logging.handlers @@ -340,6 +341,10 @@ class HelpfulArgumentParser(object): if self.detect_defaults: return parsed_args + global DEFAULTS # pylint: disable=global-statement + DEFAULTS = dict((key, copy.deepcopy(self.parser.get_default(key))) + for key in vars(parsed_args)) + # Do any post-parsing homework here if self.verb == "renew" and not parsed_args.dialog_mode: From b57677b16a994d21659ea2eed7557c9a583712d1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 14:57:14 -0700 Subject: [PATCH 183/192] Use cli.DEFAULTS in storage.py --- certbot/storage.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/certbot/storage.py b/certbot/storage.py index 60886e306..7602ece2d 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -162,30 +162,16 @@ def relevant_values(all_values): from certbot import cli - def _is_cli_default(option, value): - # Look through the CLI parser defaults and see if this option is - # both present and equal to the specified value. If not, return - # False. - # pylint: disable=protected-access - for x in cli.helpful_parser.parser._actions: - if x.dest == option: - if x.default == value: - return True - else: - break - return False - values = dict() for option, value in all_values.iteritems(): # Try to find reasons to store this item in the # renewal config. It can be stored if it is relevant and - # (it is set_by_cli() or flag_default() is different - # from the value or flag_default() doesn't exist). + # (it is set_by_cli(), we don't know the default value, or + # the current value differs from the default value). if _relevant(option): - if (cli.set_by_cli(option) - or not _is_cli_default(option, value)): -# or option not in constants.CLI_DEFAULTS -# or constants.CLI_DEFAULTS[option] != value): + if (cli.set_by_cli(option) or + option not in cli.DEFAULTS or + cli.DEFAULTS[option] != value): values[option] = value return values From 26316fb222cb3cf180810bbf8cdea85e8ddeaf95 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 15:06:49 -0700 Subject: [PATCH 184/192] Ensure changes to webroot_map aren't reflected in cli.DEFAULTS --- certbot/tests/cli_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index f9557abfb..03edaa18f 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -490,6 +490,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods conflicts += ['--staging'] self._check_server_conflict_message(short_args, conflicts) + def test_defaults_global(self): + namespace = self._get_argument_parser()([]) + namespace.webroot_map['example.com'] = '/var/www/html' + + self.assertTrue(cli.DEFAULTS != namespace.webroot_map) + def _certonly_new_request_common(self, mock_client, args=None): with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: mock_renewal.return_value = ("newcert", None) From 8f6866309781f656725c0ef9d6e89a7b291f4aed Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 16:52:35 -0700 Subject: [PATCH 185/192] Add option_was_set function --- certbot/cli.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index 3c65527e1..428227657 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -217,6 +217,21 @@ def set_by_cli(var): set_by_cli.detector = None +def option_was_set(option, value): + """Was option set by the user or does it differ from the default? + + :param str option: configuration variable being considered + :param value: value of the configuration variable named option + + :returns: True if the option was set, otherwise, False + :rtype: bool + + """ + return (set_by_cli(option) or + option not in DEFAULTS or + DEFAULTS[option] != value) + + def argparse_type(variable): "Return our argparse type function for a config variable (default: str)" # pylint: disable=protected-access From aaf93b65b0b1b08bec02e84ea92145d3f15e7081 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 16:55:45 -0700 Subject: [PATCH 186/192] Refactor storage.relevant_values --- certbot/storage.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/certbot/storage.py b/certbot/storage.py index 7602ece2d..82fdbfd54 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -7,8 +7,10 @@ import re import configobj import parsedatetime import pytz +import six import certbot +from certbot import cli from certbot import constants from certbot import crypto_util from certbot import errors @@ -158,22 +160,13 @@ def relevant_values(all_values): :param dict all_values: The original values. :returns: A new dictionary containing items that can be used in renewal. - :rtype dict:""" + :rtype dict: - from certbot import cli - - values = dict() - for option, value in all_values.iteritems(): - # Try to find reasons to store this item in the - # renewal config. It can be stored if it is relevant and - # (it is set_by_cli(), we don't know the default value, or - # the current value differs from the default value). - if _relevant(option): - if (cli.set_by_cli(option) or - option not in cli.DEFAULTS or - cli.DEFAULTS[option] != value): - values[option] = value - return values + """ + return dict( + (option, value) + for option, value in six.iteritems(all_values) + if _relevant(option) and cli.option_was_set(option, value)) class RenewableCert(object): # pylint: disable=too-many-instance-attributes From 8c174f38b5e59ecf2298abf08a84c52110cc4091 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:00:59 -0700 Subject: [PATCH 187/192] Add has_default_value method --- certbot/cli.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 428227657..3c18c3c98 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -217,6 +217,21 @@ def set_by_cli(var): set_by_cli.detector = None +def has_default_value(option, value): + """Does option have the default value? + + If the default value of option is not known, False is returned. + + :param str option: configuration variable being considered + :param value: value of the configuration variable named option + + :returns: True if option has the default value, otherwise, False + :rtype: bool + + """ + return option in DEFAULTS and DEFAULTS[option] == value + + def option_was_set(option, value): """Was option set by the user or does it differ from the default? @@ -227,9 +242,7 @@ def option_was_set(option, value): :rtype: bool """ - return (set_by_cli(option) or - option not in DEFAULTS or - DEFAULTS[option] != value) + return set_by_cli(option) or not has_default_value(option, value) def argparse_type(variable): From 4c68792dd37cc93e17038eba7cd8bee94cdcc6c1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:29:56 -0700 Subject: [PATCH 188/192] Remove test_defaults_global --- certbot/tests/cli_test.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 03edaa18f..f9557abfb 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -490,12 +490,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods conflicts += ['--staging'] self._check_server_conflict_message(short_args, conflicts) - def test_defaults_global(self): - namespace = self._get_argument_parser()([]) - namespace.webroot_map['example.com'] = '/var/www/html' - - self.assertTrue(cli.DEFAULTS != namespace.webroot_map) - def _certonly_new_request_common(self, mock_client, args=None): with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: mock_renewal.return_value = ("newcert", None) From 97af8dfb701edece39ae429852ac1a9784f991e2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:31:57 -0700 Subject: [PATCH 189/192] Add defaults to helpful_parser --- certbot/cli.py | 14 +++++--------- certbot/tests/storage_test.py | 5 ++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 3c18c3c98..ba1f23708 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -105,11 +105,6 @@ ZERO_ARG_ACTIONS = set(("store_const", "store_true", "store_false", "append_const", "count",)) -# Maps a config option to its default value. This is set during the -# parse_args method of HelpfulArgumentParser. -DEFAULTS = None - - # Maps a config option to a set of config options that may have modified it. # This dictionary is used recursively, so if A modifies B and B modifies C, # it is determined that C was modified by the user if A was modified. @@ -229,7 +224,8 @@ def has_default_value(option, value): :rtype: bool """ - return option in DEFAULTS and DEFAULTS[option] == value + return (option in helpful_parser.defaults and + helpful_parser.defaults[option] == value) def option_was_set(option, value): @@ -354,6 +350,7 @@ class HelpfulArgumentParser(object): sys.exit(0) self.visible_topics = self.determine_help_topics(self.help_arg) self.groups = {} # elements are added by .add_group() + self.defaults = {} # elements are added by .parse_args() def parse_args(self): """Parses command line arguments and returns the result. @@ -369,9 +366,8 @@ class HelpfulArgumentParser(object): if self.detect_defaults: return parsed_args - global DEFAULTS # pylint: disable=global-statement - DEFAULTS = dict((key, copy.deepcopy(self.parser.get_default(key))) - for key in vars(parsed_args)) + self.defaults = dict((key, copy.deepcopy(self.parser.get_default(key))) + for key in vars(parsed_args)) # Do any post-parsing homework here diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0d907eca3..138f6e2fa 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -533,10 +533,9 @@ class RenewableCertTests(BaseRenewableCertTest): """Test that relevant_values() can reject a default value.""" # pylint: disable=protected-access from certbot import storage - mock_parser.verb = "certonly" mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] + mock_parser.defaults = {"rsa_key_size": 2048} + mock_parser.verb = "certonly" self.assertEqual(storage.relevant_values({"rsa_key_size": 2048}), {}) @mock.patch("certbot.cli.helpful_parser") From f9d5ecaf6fc6776988ddf5ea9dbb010ad45eeb7f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:45:47 -0700 Subject: [PATCH 190/192] Add option_was_set test --- certbot/tests/cli_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index f9557abfb..9d6335b40 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -447,6 +447,19 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods short_args += '--server example.com'.split() self._check_server_conflict_message(short_args, '--staging') + def test_option_was_set(self): + key_size_option = 'rsa_key_size' + key_size_value = cli.flag_default(key_size_option) + self._get_argument_parser()( + '--rsa-key-size {0}'.format(key_size_value).split()) + + self.assertTrue(cli.option_was_set(key_size_option, key_size_value)) + self.assertTrue(cli.option_was_set('no_verify_ssl', True)) + + config_dir_option = 'config_dir' + self.assertFalse(cli.option_was_set( + config_dir_option, cli.flag_default(config_dir_option))) + def _assert_dry_run_flag_worked(self, namespace, existing_account): self.assertTrue(namespace.dry_run) self.assertTrue(namespace.break_my_certs) From 61b77766c26233745b922ef8777e6694c49ba053 Mon Sep 17 00:00:00 2001 From: Ben Irving Date: Tue, 14 Jun 2016 11:28:29 -0700 Subject: [PATCH 191/192] Add integration test cases for must staple and ECDSA (#3158) --- tests/boulder-integration.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 323ea004b..ab8fde5f6 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -84,6 +84,24 @@ if [ "$size1" -lt 3000 ] || [ "$size2" -lt 3000 ] || [ "$size3" -gt 1800 ] ; the exit 1 fi +# ECDSA +openssl ecparam -genkey -name secp384r1 -out "${root}/privkey-p384.pem" +SAN="DNS:ecdsa.le.wtf" openssl req -new -sha256 \ + -config "${OPENSSL_CNF:-openssl.cnf}" \ + -key "${root}/privkey-p384.pem" \ + -subj "/" \ + -reqexts san \ + -outform der \ + -out "${root}/csr-p384.der" +common auth --csr "${root}/csr-p384.der" \ + --cert-path "${root}/csr/cert-p384.pem" \ + --chain-path "${root}/csr/chain-p384.pem" +openssl x509 -in "${root}/csr/cert-p384.pem" -text | grep 'ASN1 OID: secp384r1' + +# OCSP Must Staple +common auth --must-staple --domains "must-staple.le.wtf" +openssl x509 -in "${root}/conf/live/must-staple.le.wtf/cert.pem" -text | grep '1.3.6.1.5.5.7.1.24' + # revoke by account key common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" # revoke renewed From 261046a2d7dd01a4ce5383d192ce82d34a92faf8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 14 Jun 2016 13:52:39 -0700 Subject: [PATCH 192/192] Update relevant_values tests --- certbot/tests/storage_test.py | 50 ++++++++++++++++------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 138f6e2fa..261500b98 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -11,6 +11,7 @@ import mock import pytz import certbot +from certbot import cli from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR @@ -517,38 +518,33 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values(self, mock_parser): + def _test_relevant_values_common(self, values): + option = "rsa_key_size" + mock_parser = mock.Mock(args=["--standalone"], verb="certonly", + defaults={option: cli.flag_default(option)}) + + from certbot.storage import relevant_values + with mock.patch("certbot.cli.helpful_parser", mock_parser): + return relevant_values(values) + + def test_relevant_values(self): """Test that relevant_values() can reject an irrelevant value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"hello": "there"}), {}) + self.assertEqual( + self._test_relevant_values_common({"hello": "there"}), {}) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values_default(self, mock_parser): + def test_relevant_values_default(self): """Test that relevant_values() can reject a default value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.args = ["--standalone"] - mock_parser.defaults = {"rsa_key_size": 2048} - mock_parser.verb = "certonly" - self.assertEqual(storage.relevant_values({"rsa_key_size": 2048}), {}) + option = "rsa_key_size" + values = {option: cli.flag_default(option)} + self.assertEqual(self._test_relevant_values_common(values), {}) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values_nondefault(self, mock_parser): + def test_relevant_values_nondefault(self): """Test that relevant_values() can retain a non-default value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"rsa_key_size": 12}), - {"rsa_key_size": 12}) + values = {"rsa_key_size": 12} + # A copy is given to _test_relevant_values_common + # to make sure values isn't modified by the method + self.assertEqual( + self._test_relevant_values_common(values.copy()), values) @mock.patch("certbot.storage.relevant_values") def test_new_lineage(self, mock_rv):