From a74eff5fbd4f68a584bff9d2805f28954d95f978 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 28 Aug 2015 06:40:19 +0000 Subject: [PATCH 001/145] Revert "Revert PR #708." This reverts commit 70e311b43f6910c070dce3bab3396db495636d00. --- acme/setup.py | 10 +++++++--- letsencrypt-apache/setup.py | 8 +++++++- letsencrypt-compatibility-test/setup.py | 8 +++++++- letsencrypt-nginx/setup.py | 8 +++++++- letshelp-letsencrypt/setup.py | 8 ++++---- setup.py | 10 +++++++--- 6 files changed, 39 insertions(+), 13 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 6d8208414..5f1da2391 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -9,7 +9,6 @@ install_requires = [ # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) 'cryptography>=0.8', - 'mock<1.1.0', # py26 'pyrfc3339', 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'pyasn1', # urllib3 InsecurePlatformWarning (#304) @@ -23,8 +22,13 @@ install_requires = [ # env markers in extras_require cause problems with older pip: #517 if sys.version_info < (2, 7): - # only some distros recognize stdlib argparse as already satisfying - install_requires.append('argparse') + install_requires.extend([ + # only some distros recognize stdlib argparse as already satisfying + 'argparse', + 'mock<1.1.0', + ]) +else: + install_requires.append('mock') testing_extras = [ 'nose', diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 39f4b68e1..5f1b0a95d 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -1,3 +1,5 @@ +import sys + from setuptools import setup from setuptools import find_packages @@ -5,12 +7,16 @@ from setuptools import find_packages install_requires = [ 'acme', 'letsencrypt', - 'mock<1.1.0', # py26 'python-augeas', 'zope.component', 'zope.interface', ] +if sys.version_info < (2, 7): + install_requires.append('mock<1.1.0') +else: + install_requires.append('mock') + setup( name='letsencrypt-apache', packages=find_packages(), diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index f02041e55..99e66f54f 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -1,3 +1,5 @@ +import sys + from setuptools import setup from setuptools import find_packages @@ -7,10 +9,14 @@ install_requires = [ 'letsencrypt-apache', 'letsencrypt-nginx', 'docker-py', - 'mock<1.1.0', # py26 'zope.interface', ] +if sys.version_info < (2, 7): + install_requires.append('mock<1.1.0') +else: + install_requires.append('mock') + setup( name='letsencrypt-compatibility-test', packages=find_packages(), diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 92b974974..64742e7b6 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -1,3 +1,5 @@ +import sys + from setuptools import setup from setuptools import find_packages @@ -6,10 +8,14 @@ install_requires = [ 'acme', 'letsencrypt', 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? - 'mock<1.1.0', # py26 'zope.interface', ] +if sys.version_info < (2, 7): + install_requires.append('mock<1.1.0') +else: + install_requires.append('mock') + setup( name='letsencrypt-nginx', packages=find_packages(), diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index 6b89a6d09..7d42af88b 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -6,17 +6,17 @@ from setuptools import find_packages install_requires = [] if sys.version_info < (2, 7): - install_requires.append("mock<1.1.0") + install_requires.append('mock<1.1.0') else: - install_requires.append("mock") + install_requires.append('mock') setup( - name="letshelp-letsencrypt", + name='letshelp-letsencrypt', packages=find_packages(), install_requires=install_requires, entry_points={ 'console_scripts': [ - "letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main", + 'letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main', ], }, include_package_data=True, diff --git a/setup.py b/setup.py index f816c6c56..e15ade595 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,6 @@ install_requires = [ 'ConfigArgParse', 'configobj', 'cryptography>=0.7', # load_pem_x509_certificate - 'mock<1.1.0', # py26 'parsedatetime', 'psutil>=2.1.0', # net_connections introduced in 2.1.0 'PyOpenSSL', @@ -47,8 +46,13 @@ install_requires = [ # env markers in extras_require cause problems with older pip: #517 if sys.version_info < (2, 7): - # only some distros recognize stdlib argparse as already satisfying - install_requires.append('argparse') + install_requires.extend([ + # only some distros recognize stdlib argparse as already satisfying + 'argparse', + 'mock<1.1.0', + ]) +else: + install_requires.append('mock') dev_extras = [ # Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289 From 1bb62eed4dd97067ae35e922db3669701dc0a3fe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 10 Sep 2015 22:35:44 -0400 Subject: [PATCH 002/145] Started crash recovery mechanism --- letsencrypt/error_handler.py | 46 ++++++++++++++++++++++++++++++++++++ letsencrypt/interfaces.py | 11 +++++++++ 2 files changed, 57 insertions(+) create mode 100644 letsencrypt/error_handler.py diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py new file mode 100644 index 000000000..884c73927 --- /dev/null +++ b/letsencrypt/error_handler.py @@ -0,0 +1,46 @@ +"""Registers and calls cleanup functions in case of an error.""" +import os +import signal + + +_SIGNALS = [signal.SIGTERM] if os.name == "nt" else + [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, + signal.SIGXCPU, signal.SIGXFSZ, signal.SIGPWR,] + + +class ErrorHandler(): + """Registers and calls cleanup functions in case of an error.""" + def __init__(self, func=None): + self.funcs = [] + if func: + self.funcs.append(func) + + def __enter__(self): + self.set_signal_handlers() + + def __exit__(self, exec_type, exec_value, traceback): + if exec_value is not None: + self.cleanup() + self.reset_signal_handlers() + + def register(self, func): + """Registers func to be called if an error occurs.""" + self.funcs.append(func) + + def cleanup(self): + """Calls all registered functions.""" + while self.funcs: + self.funcs.pop()() + + def set_signal_handlers(self): + for signal_type in _SIGNALS: + signal.signal(signal_type, self._signal_handler) + + def reset_signal_handlers(self): + for signal_type in _SIGNALS: + signal.signal(signal_type, signal.SIG_DFL) + + def _signal_handler(self, signum, frame): + self.cleanup() + signal.signal(signal_type, signal.SIG_DFL) + os.kill(os.getpid(), signum) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index f330e28ce..653b5685b 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -322,6 +322,17 @@ class IInstaller(IPlugin): """ + def recovery_routine(self): + """Revert configuration to most recent finalized checkpoint. + + Remove all changes (temporary and permanent) that have not been + finalized. This is useful to protect against crashes and other + execution interruptions. + + :raises .errors.PluginError: If unable to recover the configuration + + """ + def view_config_changes(): """Display all of the LE config changes. From c025c17b5de7d5b328c8143ed9d3c32075d9df32 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 15 Sep 2015 22:48:36 -0700 Subject: [PATCH 003/145] auth use renewal --- letsencrypt/cli.py | 189 ++++++++++++++++++------------ letsencrypt/client.py | 10 +- letsencrypt/tests/renewer_test.py | 5 + 3 files changed, 121 insertions(+), 83 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 3a600d7f7..825cae775 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -196,8 +196,101 @@ def _find_duplicative_certs(domains, config, renew_config): return identical_names_cert, subset_names_cert +def _treat_as_renewal(config, domains): + """Determine whether or not the call should be treated as a renewal. + + :returns: RenewableCert or None if renewal shouldn't occur. + :rtype: :class:`.storage.RenewableCert` + + :raises .Error: If the user would like to rerun the client again. + + """ + renewal = False + + # Considering the possibility that the requested certificate is + # related to an existing certificate. (config.duplicate, which + # is set with --duplicate, skips all of this logic and forces any + # kind of certificate to be obtained with renewal = False.) + if not config.duplicate: + ident_names_cert, subset_names_cert = _find_duplicative_certs( + domains, config, configuration.RenewerConfiguration(config)) + # I am not sure whether that correctly reads the systemwide + # configuration file. + question = None + if ident_names_cert is not None: + question = ( + "You have an existing certificate that contains exactly the " + "same domains you requested (ref: {0})\n\nDo you want to " + "renew and replace this certificate with a newly-issued one?" + ).format(ident_names_cert.configfile.filename) + elif subset_names_cert is not None: + question = ( + "You have an existing certificate that contains a portion of " + "the domains you requested (ref: {0})\n\nIt contains these " + "names: {1}\n\nYou requested these names for the new " + "certificate: {2}.\n\nDo you want to replace this existing " + "certificate with the new certificate?" + ).format(subset_names_cert.configfile.filename, + ", ".join(subset_names_cert.names()), + ", ".join(domains)) + if question is None: + # We aren't in a duplicative-names situation at all, so we don't + # have to tell or ask the user anything about this. + pass + elif zope.component.getUtility(interfaces.IDisplay).yesno( + question, "Replace", "Cancel"): + renewal = True + else: + reporter_util = zope.component.getUtility(interfaces.IReporter) + reporter_util.add_message( + "To obtain a new certificate that {0} an existing certificate " + "in its domain-name coverage, you must use the --duplicate " + "option.\n\nFor example:\n\n{1} --duplicate {2}".format( + "duplicates" if ident_names_cert is not None else + "overlaps with", sys.argv[0], " ".join(sys.argv[1:])), + reporter_util.HIGH_PRIORITY) + raise errors.Error( + "BUser did not use proper CLI and would like " + "to reinvoke the client.") + + if renewal: + return ident_names_cert if ident_names_cert is not None else subset_names_cert + + return None + + +def auth_from_domains(le_client, config, domains, plugins): + """Authenticate and enroll certificate.""" + # Note: This can raise errors... caught above us though. + lineage = _treat_as_renewal(config, domains) + + if lineage is not None: + new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) + # TODO: Check whether it worked! + lineage.save_successor( + lineage.latest_common_version(), OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_PEM, new_certr.body), + new_key.pem, OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_PEM, new_chain)) + + lineage.update_all_links_to(lineage.latest_common_version()) + # TODO: Check return value of save_successor + # TODO: Also update lineage renewal config with any relevant + # configuration values from this attempt? - YES + else: + # TREAT AS NEW REQUEST + lineage = le_client.obtain_and_enroll_certificate( + domains, le_client.dv_auth, le_client.installer, plugins) + if not lineage: + raise errors.Error("Certificate could not be obtained") + + return lineage + +# TODO: Make run as close to auth + install as possible +# Possible difficulties: args.csr was hacked into auth def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals """Obtain a certificate and install.""" + # Begin authenticator and installer setup if args.configurator is not None and (args.installer is not None or args.authenticator is not None): return ("Either --configurator or --authenticator/--installer" @@ -216,88 +309,28 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo if installer is None or authenticator is None: return "Configurator could not be determined" + # End authenticator and installer setup domains = _find_domains(args, installer) - treat_as_renewal = False - - # Considering the possibility that the requested certificate is - # related to an existing certificate. (config.duplicate, which - # is set with --duplicate, skips all of this logic and forces any - # kind of certificate to be obtained with treat_as_renewal = False.) - if not config.duplicate: - identical_names_cert, subset_names_cert = _find_duplicative_certs( - domains, config, configuration.RenewerConfiguration(config)) - # I am not sure whether that correctly reads the systemwide - # configuration file. - question = None - if identical_names_cert is not None: - question = ( - "You have an existing certificate that contains exactly the " - "same domains you requested (ref: {0})\n\nDo you want to " - "renew and replace this certificate with a newly-issued one?" - ).format(identical_names_cert.configfile.filename) - elif subset_names_cert is not None: - question = ( - "You have an existing certificate that contains a portion of " - "the domains you requested (ref: {0})\n\nIt contains these " - "names: {1}\n\nYou requested these names for the new " - "certificate: {2}.\n\nDo you want to replace this existing " - "certificate with the new certificate?" - ).format(subset_names_cert.configfile.filename, - ", ".join(subset_names_cert.names()), - ", ".join(domains)) - if question is None: - # We aren't in a duplicative-names situation at all, so we don't - # have to tell or ask the user anything about this. - pass - elif zope.component.getUtility(interfaces.IDisplay).yesno( - question, "Replace", "Cancel"): - treat_as_renewal = True - else: - reporter_util = zope.component.getUtility(interfaces.IReporter) - reporter_util.add_message( - "To obtain a new certificate that {0} an existing certificate " - "in its domain-name coverage, you must use the --duplicate " - "option.\n\nFor example:\n\n{1} --duplicate {2}".format( - "duplicates" if identical_names_cert is not None else - "overlaps with", sys.argv[0], " ".join(sys.argv[1:])), - reporter_util.HIGH_PRIORITY) - return 1 - # Attempting to obtain the certificate # TODO: Handle errors from _init_le_client? le_client = _init_le_client(args, config, authenticator, installer) - if treat_as_renewal: - lineage = identical_names_cert if identical_names_cert is not None else subset_names_cert - # TODO: Use existing privkey instead of generating a new one - new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) - # TODO: Check whether it worked! - lineage.save_successor( - lineage.latest_common_version(), OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, new_certr.body), - new_key.pem, OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, new_chain)) - lineage.update_all_links_to(lineage.latest_common_version()) - # TODO: Check return value of save_successor - # TODO: Also update lineage renewal config with any relevant - # configuration values from this attempt? - le_client.deploy_certificate( - domains, lineage.privkey, lineage.cert, lineage.chain) - display_ops.success_renewal(domains) - else: - # TREAT AS NEW REQUEST - lineage = le_client.obtain_and_enroll_certificate( - domains, authenticator, installer, plugins) - if not lineage: - return "Certificate could not be obtained" - # TODO: This treats the key as changed even when it wasn't - # TODO: We also need to pass the fullchain (for Nginx) - le_client.deploy_certificate( - domains, lineage.privkey, lineage.cert, lineage.chain) - le_client.enhance_config(domains, args.redirect) + try: + lineage = auth_from_domains(le_client, config, domains, plugins) + except errors.Error as err: + return str(err) + + # TODO: We also need to pass the fullchain (for Nginx) + le_client.deploy_certificate( + domains, lineage.privkey, lineage.cert, lineage.chain) + le_client.enhance_config(domains, args.redirect) + + if lineage.available_versions("cert") == 1: display_ops.success_installation(domains) + else: + display_ops.success_renewal(domains) def auth(args, config, plugins): @@ -322,6 +355,7 @@ def auth(args, config, plugins): # TODO: Handle errors from _init_le_client? le_client = _init_le_client(args, config, authenticator, installer) + # This is a special case; cert and chain are simply saved if args.csr is not None: certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR( file=args.csr[0], data=args.csr[1], form="der")) @@ -329,9 +363,10 @@ def auth(args, config, plugins): certr, chain, args.cert_path, args.chain_path) else: domains = _find_domains(args, installer) - if not le_client.obtain_and_enroll_certificate( - domains, authenticator, installer, plugins): - return "Certificate could not be obtained" + try: + auth_from_domains(le_client, config, domains, plugins) + except errors.Error as err: + return str(err) def install(args, config, plugins): diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 7f40fef5b..131b0b9f0 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -111,6 +111,8 @@ class Client(object): :ivar .AuthHandler auth_handler: Authorizations handler that will dispatch DV and Continuity challenges to appropriate authenticators (providing `.IAuthenticator` interface). + :ivar .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`) + authenticator that can solve the `.constants.DV_CHALLENGES`. :ivar .IInstaller installer: Installer. :ivar acme.client.Client acme: Optional ACME client API handle. You might already have one from `register`. @@ -118,14 +120,10 @@ class Client(object): """ def __init__(self, config, account_, dv_auth, installer, acme=None): - """Initialize a client. - - :param .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`) - authenticator that can solve the `.constants.DV_CHALLENGES`. - - """ + """Initialize a client.""" self.config = config self.account = account_ + self.dv_auth = dv_auth self.installer = installer # Initialize ACME if account is provided diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index abf7298b2..1235ee329 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -33,7 +33,12 @@ def fill_with_sample_data(rc_object): class BaseRenewableCertTest(unittest.TestCase): + """Base class for setting up Renewable Cert tests. + .. note:: It may be required to write out self.config for + your test. Check :class:`.cli_test.DuplicateCertTest` for an example. + + """ def setUp(self): from letsencrypt import storage self.tempdir = tempfile.mkdtemp() From 23edd48d5adc3de7fdcffc459bcd7eed9f9c0ceb Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 15 Sep 2015 23:34:00 -0700 Subject: [PATCH 004/145] minor fixes --- letsencrypt/cli.py | 9 ++++----- letsencrypt/tests/cli_test.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 35a17d0a7..11496b231 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -250,7 +250,7 @@ def _treat_as_renewal(config, domains): "overlaps with", sys.argv[0], " ".join(sys.argv[1:])), reporter_util.HIGH_PRIORITY) raise errors.Error( - "BUser did not use proper CLI and would like " + "User did not use proper CLI and would like " "to reinvoke the client.") if renewal: @@ -259,7 +259,7 @@ def _treat_as_renewal(config, domains): return None -def auth_from_domains(le_client, config, domains, plugins): +def _auth_from_domains(le_client, config, domains, plugins): """Authenticate and enroll certificate.""" # Note: This can raise errors... caught above us though. lineage = _treat_as_renewal(config, domains) @@ -312,12 +312,11 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo domains = _find_domains(args, installer) - # Attempting to obtain the certificate # TODO: Handle errors from _init_le_client? le_client = _init_le_client(args, config, authenticator, installer) try: - lineage = auth_from_domains(le_client, config, domains, plugins) + lineage = _auth_from_domains(le_client, config, domains, plugins) except errors.Error as err: return str(err) @@ -363,7 +362,7 @@ def auth(args, config, plugins): else: domains = _find_domains(args, installer) try: - auth_from_domains(le_client, config, domains, plugins) + _auth_from_domains(le_client, config, domains, plugins) except errors.Error as err: return str(err) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 2da1272fc..584e67feb 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -206,5 +206,5 @@ class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest): self.assertEqual(result, (None, None)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() # pragma: no cover From a0d67aeed7eb0a853ce9edebda62213adcdd81ee Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 16 Sep 2015 01:25:08 -0700 Subject: [PATCH 005/145] correct success message for 'run' --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 11496b231..040be2b03 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -325,7 +325,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo domains, lineage.privkey, lineage.cert, lineage.chain) le_client.enhance_config(domains, args.redirect) - if lineage.available_versions("cert") == 1: + if len(lineage.available_versions("cert")) == 1: display_ops.success_installation(domains) else: display_ops.success_renewal(domains) From 8b9a66d7ddcd8fbf6d6bfad2ec214ae6ece86bbf Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 16 Sep 2015 12:33:56 -0700 Subject: [PATCH 006/145] Make sure configs directory exists --- letsencrypt/cli.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 040be2b03..b4649a29a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -172,6 +172,9 @@ def _find_duplicative_certs(domains, config, renew_config): identical_names_cert, subset_names_cert = None, None configs_dir = renew_config.renewal_configs_dir + # Verify the directory is there + le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) + cli_config = configuration.RenewerConfiguration(config) for renewal_file in os.listdir(configs_dir): try: @@ -220,15 +223,15 @@ def _treat_as_renewal(config, domains): if ident_names_cert is not None: question = ( "You have an existing certificate that contains exactly the " - "same domains you requested (ref: {0})\n\nDo you want to " + "same domains you requested (ref: {0}){br}{br}Do you want to " "renew and replace this certificate with a newly-issued one?" ).format(ident_names_cert.configfile.filename) elif subset_names_cert is not None: question = ( "You have an existing certificate that contains a portion of " - "the domains you requested (ref: {0})\n\nIt contains these " - "names: {1}\n\nYou requested these names for the new " - "certificate: {2}.\n\nDo you want to replace this existing " + "the domains you requested (ref: {0}){br}{br}It contains these " + "names: {1}{br}{br}You requested these names for the new " + "certificate: {2}.{br}{br}Do you want to replace this existing " "certificate with the new certificate?" ).format(subset_names_cert.configfile.filename, ", ".join(subset_names_cert.names()), @@ -245,7 +248,7 @@ def _treat_as_renewal(config, domains): reporter_util.add_message( "To obtain a new certificate that {0} an existing certificate " "in its domain-name coverage, you must use the --duplicate " - "option.\n\nFor example:\n\n{1} --duplicate {2}".format( + "option.{br}{br}For example:{br}{br}{1} --duplicate {2}".format( "duplicates" if ident_names_cert is not None else "overlaps with", sys.argv[0], " ".join(sys.argv[1:])), reporter_util.HIGH_PRIORITY) @@ -285,7 +288,7 @@ def _auth_from_domains(le_client, config, domains, plugins): return lineage -# TODO: Make run as close to auth + install as possible +# TODO: Make run as close to auth + install as possible # Possible difficulties: args.csr was hacked into auth def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals """Obtain a certificate and install.""" From f582a85314f2b09d42c97a02846d77c5c62eea0c Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 16 Sep 2015 13:03:42 -0700 Subject: [PATCH 007/145] mock out make_or_verify --- letsencrypt/tests/cli_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 584e67feb..c38ece0e1 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -176,7 +176,8 @@ class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest): def tearDown(self): shutil.rmtree(self.tempdir) - def test_find_duplicative_names(self): + @mock.patch("letsencrypt.le_util.make_or_verify_dir") + def test_find_duplicative_names(self, unused): from letsencrypt.cli import _find_duplicative_certs test_cert = test_util.load_vector("cert-san.pem") with open(self.test_rc.cert, "w") as f: From e8611d299ad490fbefffd039d90d1f2676fd6ebb Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 16 Sep 2015 13:23:46 -0700 Subject: [PATCH 008/145] Cleanup formatting issues --- letsencrypt/cli.py | 16 +++++++++++----- letsencrypt/tests/cli_test.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index b4649a29a..88266aaeb 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -225,7 +225,7 @@ def _treat_as_renewal(config, domains): "You have an existing certificate that contains exactly the " "same domains you requested (ref: {0}){br}{br}Do you want to " "renew and replace this certificate with a newly-issued one?" - ).format(ident_names_cert.configfile.filename) + ).format(ident_names_cert.configfile.filename, br=os.linesep) elif subset_names_cert is not None: question = ( "You have an existing certificate that contains a portion of " @@ -235,7 +235,8 @@ def _treat_as_renewal(config, domains): "certificate with the new certificate?" ).format(subset_names_cert.configfile.filename, ", ".join(subset_names_cert.names()), - ", ".join(domains)) + ", ".join(domains), + br=os.linesep) if question is None: # We aren't in a duplicative-names situation at all, so we don't # have to tell or ask the user anything about this. @@ -250,7 +251,10 @@ def _treat_as_renewal(config, domains): "in its domain-name coverage, you must use the --duplicate " "option.{br}{br}For example:{br}{br}{1} --duplicate {2}".format( "duplicates" if ident_names_cert is not None else - "overlaps with", sys.argv[0], " ".join(sys.argv[1:])), + "overlaps with", + sys.argv[0], " ".join(sys.argv[1:]), + br=os.linesep + ), reporter_util.HIGH_PRIORITY) raise errors.Error( "User did not use proper CLI and would like " @@ -288,6 +292,7 @@ def _auth_from_domains(le_client, config, domains, plugins): return lineage + # TODO: Make run as close to auth + install as possible # Possible difficulties: args.csr was hacked into auth def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals @@ -708,7 +713,7 @@ def create_parser(plugins, args): # For now unfortunately this constant just needs to match the code below; # there isn't an elegant way to autogenerate it in time. -VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes",\ +VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", "plugins"] @@ -862,7 +867,8 @@ def _handle_exception(exc_type, exc_value, trace, args): """ logger.debug( - "Exiting abnormally:\n%s", + "Exiting abnormally:%s%s", + os.linesep, "".join(traceback.format_exception(exc_type, exc_value, trace))) if issubclass(exc_type, Exception) and (args is None or not args.debug): diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index c38ece0e1..97725a4c7 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -177,7 +177,7 @@ class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest): shutil.rmtree(self.tempdir) @mock.patch("letsencrypt.le_util.make_or_verify_dir") - def test_find_duplicative_names(self, unused): + def test_find_duplicative_names(self, unused): # pylint: disable=unused-argument from letsencrypt.cli import _find_duplicative_certs test_cert = test_util.load_vector("cert-san.pem") with open(self.test_rc.cert, "w") as f: From 740f516561200a43c4720fcd7aad8346a38de77d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 16 Sep 2015 19:09:04 -0700 Subject: [PATCH 009/145] Make boulder-start.sh more robust & helpful --- tests/boulder-start.sh | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/boulder-start.sh b/tests/boulder-start.sh index e17716b54..0af5dfb97 100755 --- a/tests/boulder-start.sh +++ b/tests/boulder-start.sh @@ -1,6 +1,23 @@ -#!/bin/sh -xe +#!/bin/bash # Download and run Boulder instance for integration testing + +# ugh, go version output is like: +# go version go1.4.2 linux/amd64 +GOVER=`go version | cut -d" " -f3 | cut -do -f2` + +# version comparison +function verlte { + [ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ] +} + +if ! verlte 1.5 "$GOVER" ; then + echo "We require go version 1.5 or later; you have... $GOVER" + exit 1 +fi + +set -xe + export GOPATH="${GOPATH:-/tmp/go}" # `/...` avoids `no buildable Go source files` errors, for more info @@ -8,7 +25,11 @@ export GOPATH="${GOPATH:-/tmp/go}" go get -d github.com/letsencrypt/boulder/... cd $GOPATH/src/github.com/letsencrypt/boulder # goose is needed for ./test/create_db.sh -go get bitbucket.org/liamstask/goose/cmd/goose +if ! go get bitbucket.org/liamstask/goose/cmd/goose ; then + echo Problems installing goose... perhaps rm -rf \$GOPATH \("$GOPATH"\) + echo and try again... + exit 1 +fi ./test/create_db.sh ./start.py & # Hopefully start.py bootstraps before integration test is started... From cf08fe799ae48606c8aded138397c0288f5c65e0 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 21 Sep 2015 16:57:28 -0700 Subject: [PATCH 010/145] fix #799 --- letsencrypt/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index dcd1e55b1..e4787a849 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -172,6 +172,8 @@ def _find_duplicative_certs(domains, config, renew_config): identical_names_cert, subset_names_cert = None, None configs_dir = renew_config.renewal_configs_dir + le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) + cli_config = configuration.RenewerConfiguration(config) for renewal_file in os.listdir(configs_dir): try: From 6e4faac9c07297beeb56c83a5239eea3fed767a2 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 22 Sep 2015 08:15:11 -0700 Subject: [PATCH 011/145] Allow single/double quotes around Include dirs --- letsencrypt-apache/letsencrypt_apache/parser.py | 3 +++ .../letsencrypt_apache/tests/complex_parsing_test.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/parser.py b/letsencrypt-apache/letsencrypt_apache/parser.py index 823d9794b..e70d22d4e 100644 --- a/letsencrypt-apache/letsencrypt_apache/parser.py +++ b/letsencrypt-apache/letsencrypt_apache/parser.py @@ -390,6 +390,9 @@ class ApacheParser(object): # logger.error("Error: Invalid regexp characters in %s", arg) # return [] + # Remove beginning and ending quotes + arg = arg.strip("'\"") + # Standardize the include argument based on server root if not arg.startswith("/"): # Normpath will condense ../ diff --git a/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py b/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py index 64ecaa321..a373b9766 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py @@ -78,7 +78,7 @@ class ComplexParserTest(util.ParserTest): # This is in an IfDefine self.assertTrue("ssl_module" in self.parser.modules) self.assertTrue("mod_ssl.c" in self.parser.modules) - + # def verify_fnmatch(self, arg, hit=True): """Test if Include was correctly parsed.""" from letsencrypt_apache import parser @@ -89,6 +89,7 @@ class ComplexParserTest(util.ParserTest): else: self.assertFalse(self.parser.find_dir("FNMATCH_DIRECTIVE")) + # NOTE: Only run one test per function otherwise you will have inf recursion def test_include(self): self.verify_fnmatch("test_fnmatch.?onf") @@ -101,6 +102,12 @@ class ComplexParserTest(util.ParserTest): def test_include_fullpath_trailing_slash(self): self.verify_fnmatch(self.config_path + "//") + def test_include_single_quotes(self): + self.verify_fnmatch("'" + self.config_path + "'") + + def test_include_double_quotes(self): + self.verify_fnmatch('"' + self.config_path + '"') + def test_include_variable(self): self.verify_fnmatch("../complex_parsing/${fnmatch_filename}") From 19d65c3e2f919ddc14cfc1bd34dc9b29a5c725fd Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 22 Sep 2015 08:56:26 -0700 Subject: [PATCH 012/145] Add variable quote parsing --- letsencrypt-apache/letsencrypt_apache/parser.py | 8 ++++++++ .../letsencrypt_apache/tests/complex_parsing_test.py | 7 +++++++ .../tests/testdata/complex_parsing/apache2.conf | 2 ++ .../tests/testdata/complex_parsing/test_variables.conf | 1 + 4 files changed, 18 insertions(+) diff --git a/letsencrypt-apache/letsencrypt_apache/parser.py b/letsencrypt-apache/letsencrypt_apache/parser.py index e70d22d4e..0ba438e65 100644 --- a/letsencrypt-apache/letsencrypt_apache/parser.py +++ b/letsencrypt-apache/letsencrypt_apache/parser.py @@ -315,6 +315,14 @@ class ApacheParser(object): """ value = self.aug.get(match) + + # No need to strip quotes for variables, as apache2ctl already does this + # but we do need to strip quotes for all normal arguments. + + # Note: normal argument may be a quoted variable + # e.g. strip now, not later + value = value.strip("'\"") + variables = ApacheParser.arg_var_interpreter.findall(value) for var in variables: diff --git a/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py b/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py index a373b9766..37c0208c1 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py @@ -32,6 +32,7 @@ class ComplexParserTest(util.ParserTest): "COMPLEX": "", "tls_port": "1234", "fnmatch_filename": "test_fnmatch.conf", + "tls_port_str": "1234" } ) @@ -49,6 +50,12 @@ class ComplexParserTest(util.ParserTest): self.assertEqual(len(matches), 1) self.assertEqual(self.parser.get_arg(matches[0]), "1234") + def test_basic_variable_parsing_quotes(self): + matches = self.parser.find_dir("TestVariablePortStr") + + self.assertEqual(len(matches), 1) + self.assertEqual(self.parser.get_arg(matches[0]), "1234") + def test_invalid_variable_parsing(self): del self.parser.variables["tls_port"] diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/complex_parsing/apache2.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/complex_parsing/apache2.conf index 26bf47263..14cf95f9e 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/testdata/complex_parsing/apache2.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/complex_parsing/apache2.conf @@ -46,6 +46,8 @@ IncludeOptional sites-enabled/*.conf Define COMPLEX Define tls_port 1234 +Define tls_port_str "1234" + Define fnmatch_filename test_fnmatch.conf diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/complex_parsing/test_variables.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/complex_parsing/test_variables.conf index a38191837..1a9edff74 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/testdata/complex_parsing/test_variables.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/complex_parsing/test_variables.conf @@ -1,4 +1,5 @@ TestVariablePort ${tls_port} +TestVariablePortStr "${tls_port_str}" LoadModule status_module modules/mod_status.so From 202b21f260cecbb9354e03831497ac4c38134bed Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 22 Sep 2015 08:58:02 -0700 Subject: [PATCH 013/145] Remove extra # --- .../letsencrypt_apache/tests/complex_parsing_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py b/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py index 37c0208c1..bb0ff6af9 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py @@ -85,7 +85,7 @@ class ComplexParserTest(util.ParserTest): # This is in an IfDefine self.assertTrue("ssl_module" in self.parser.modules) self.assertTrue("mod_ssl.c" in self.parser.modules) - # + def verify_fnmatch(self, arg, hit=True): """Test if Include was correctly parsed.""" from letsencrypt_apache import parser From e922a82277b52203ebfc74787e28bbd605d7d9e7 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 22 Sep 2015 09:06:53 -0700 Subject: [PATCH 014/145] letsencrypt-apache/ --- letsencrypt-apache/letsencrypt_apache/parser.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt-apache/letsencrypt_apache/parser.py b/letsencrypt-apache/letsencrypt_apache/parser.py index 0ba438e65..0a3643064 100644 --- a/letsencrypt-apache/letsencrypt_apache/parser.py +++ b/letsencrypt-apache/letsencrypt_apache/parser.py @@ -241,6 +241,10 @@ class ApacheParser(object): Directives should be in the form of a case insensitive regex currently .. todo:: arg should probably be a list + .. todo:: arg search currently only supports direct matching. It does + not handle the case of variables or quoted arguments. This should + be adapted to use a generic search for the directive and then do a + case-insensitive self.get_arg filter Note: Augeas is inherently case sensitive while Apache is case insensitive. Augeas 1.0 allows case insensitive regexes like From d4d71a73a55990d127dfb7d531f634b8b4219116 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 22 Sep 2015 09:16:49 -0700 Subject: [PATCH 015/145] Remove trailing whitespace --- .../letsencrypt_apache/tests/complex_parsing_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py b/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py index bb0ff6af9..7099c388f 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/complex_parsing_test.py @@ -85,7 +85,7 @@ class ComplexParserTest(util.ParserTest): # This is in an IfDefine self.assertTrue("ssl_module" in self.parser.modules) self.assertTrue("mod_ssl.c" in self.parser.modules) - + def verify_fnmatch(self, arg, hit=True): """Test if Include was correctly parsed.""" from letsencrypt_apache import parser From aa216a96d4ec2ede40dda8dfea81330669dca150 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 22 Sep 2015 18:24:22 -0700 Subject: [PATCH 016/145] Finished error_handler --- letsencrypt/error_handler.py | 51 +++++++++++++++---------- letsencrypt/tests/error_handler_test.py | 25 ++++++++++++ 2 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 letsencrypt/tests/error_handler_test.py diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py index 884c73927..b82f49b5a 100644 --- a/letsencrypt/error_handler.py +++ b/letsencrypt/error_handler.py @@ -3,44 +3,55 @@ import os import signal -_SIGNALS = [signal.SIGTERM] if os.name == "nt" else - [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, - signal.SIGXCPU, signal.SIGXFSZ, signal.SIGPWR,] +_SIGNALS = ([signal.SIGTERM] if os.name == "nt" else + [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, + signal.SIGXCPU, signal.SIGXFSZ, signal.SIGPWR]) -class ErrorHandler(): +class ErrorHandler(object): """Registers and calls cleanup functions in case of an error.""" def __init__(self, func=None): - self.funcs = [] - if func: - self.funcs.append(func) + self.funcs = [func] if func else [] + self.prev_handlers = {} def __enter__(self): self.set_signal_handlers() def __exit__(self, exec_type, exec_value, traceback): if exec_value is not None: - self.cleanup() + self.call_registered() self.reset_signal_handlers() def register(self, func): """Registers func to be called if an error occurs.""" self.funcs.append(func) - - def cleanup(self): - """Calls all registered functions.""" - while self.funcs: - self.funcs.pop()() + + def call_registered(self): + """Calls all functions in the order they were registered.""" + for func in self.funcs: + func() def set_signal_handlers(self): - for signal_type in _SIGNALS: - signal.signal(signal_type, self._signal_handler) + """Sets signal handlers for signals in _SIGNALS.""" + for signum in _SIGNALS: + prev_handler = signal.getsignal(signum) + # If prev_handler is None, the handler was set outside of Python + if prev_handler is not None: + self.prev_handlers[signum] = prev_handler + signal.signal(signum, self._signal_handler) def reset_signal_handlers(self): - for signal_type in _SIGNALS: - signal.signal(signal_type, signal.SIG_DFL) + """Resets signal handlers for signals in _SIGNALS.""" + for signum in self.prev_handlers: + signal.signal(signum, self.prev_handlers[signum]) + self.prev_handlers.clear() - def _signal_handler(self, signum, frame): - self.cleanup() - signal.signal(signal_type, signal.SIG_DFL) + def _signal_handler(self, signum, _): + """Calls registered functions and the previous signal handler. + + :param int signum: number of current signal + + """ + self.call_registered() + signal.signal(signum, self.prev_handlers[signum]) os.kill(os.getpid(), signum) diff --git a/letsencrypt/tests/error_handler_test.py b/letsencrypt/tests/error_handler_test.py new file mode 100644 index 000000000..6c6d02ec3 --- /dev/null +++ b/letsencrypt/tests/error_handler_test.py @@ -0,0 +1,25 @@ +"""Tests for letsencrypt.error_handler.""" +import unittest + +import mock + + +class ErrorHandlerTest(unittest.TestCase): + """Tests for letsencrypt.error_handler.""" + + def setUp(self): + from letsencrypt import error_handler + self.init_func = mock.MagicMock() + self.error_handler = error_handler.ErrorHandler(self.init_func) + + def test_context_manager(self): + try: + with self.error_handler: + raise ValueError + except ValueError: + pass + self.init_func.assert_called_once_with() + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From 2b9f72fc29c2e3bf4b223f37f4e503037b82d548 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Sep 2015 15:02:20 -0700 Subject: [PATCH 017/145] Finished basic crash recovery --- letsencrypt/auth_handler.py | 10 +++----- letsencrypt/client.py | 33 ++++++++++++++----------- letsencrypt/error_handler.py | 4 ++- letsencrypt/interfaces.py | 2 +- letsencrypt/tests/client_test.py | 33 +++++++++++++++++++++++++ letsencrypt/tests/error_handler_test.py | 27 +++++++++++++++++--- 6 files changed, 82 insertions(+), 27 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 6498a5c19..a285825dc 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -11,6 +11,7 @@ from acme import messages from letsencrypt import achallenges from letsencrypt import constants from letsencrypt import errors +from letsencrypt import error_handler from letsencrypt import interfaces @@ -106,17 +107,12 @@ class AuthHandler(object): """Get Responses for challenges from authenticators.""" cont_resp = [] dv_resp = [] - try: + logger.info("Attempting to set up challenges.") + with error_handler.ErrorHandler(self._cleanup_challenges): if self.cont_c: cont_resp = self.cont_auth.perform(self.cont_c) if self.dv_c: dv_resp = self.dv_auth.perform(self.dv_c) - # This will catch both specific types of errors. - except errors.AuthorizationError: - logger.critical("Failure in setting up challenges.") - logger.info("Attempting to clean up outstanding challenges...") - self._cleanup_challenges() - raise assert len(cont_resp) == len(self.cont_c) assert len(dv_resp) == len(self.dv_c) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 60eaea5a1..3f1f4900b 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -18,6 +18,7 @@ from letsencrypt import constants from letsencrypt import continuity_auth from letsencrypt import crypto_util from letsencrypt import errors +from letsencrypt import error_handler from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt import reverter @@ -364,16 +365,17 @@ class Client(object): chain_path = None if chain_path is None else os.path.abspath(chain_path) - for dom in domains: - # TODO: Provide a fullchain reference for installers like - # nginx that want it - self.installer.deploy_cert( - dom, os.path.abspath(cert_path), - os.path.abspath(privkey_path), chain_path) + with error_handler.ErrorHandler(self.installer.recovery_routine): + for dom in domains: + # TODO: Provide a fullchain reference for installers like + # nginx that want it + self.installer.deploy_cert( + dom, os.path.abspath(cert_path), + os.path.abspath(privkey_path), chain_path) - self.installer.save("Deployed Let's Encrypt Certificate") - # sites may have been enabled / final cleanup - self.installer.restart() + self.installer.save("Deployed Let's Encrypt Certificate") + # sites may have been enabled / final cleanup + self.installer.restart() def enhance_config(self, domains, redirect=None): """Enhance the configuration. @@ -399,6 +401,8 @@ class Client(object): if redirect is None: redirect = enhancements.ask("redirect") + # When support for more enhancements are added, the call to the + # plugin's `enhance` function should be wrapped by an ErrorHandler if redirect: self.redirect_to_ssl(domains) @@ -409,14 +413,13 @@ class Client(object): :type vhost: :class:`letsencrypt.interfaces.IInstaller` """ - for dom in domains: - try: + with error_handler.ErrorHandler(self.installer.recovery_routine): + for dom in domains: + logger.info("Attempting to perform redirect for %s", dom) self.installer.enhance(dom, "redirect") - except errors.PluginError: - logger.warn("Unable to perform redirect for %s", dom) - self.installer.save("Add Redirects") - self.installer.restart() + self.installer.save("Add Redirects") + self.installer.restart() def validate_key_csr(privkey, csr=None): diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py index b82f49b5a..3fc948b54 100644 --- a/letsencrypt/error_handler.py +++ b/letsencrypt/error_handler.py @@ -11,8 +11,10 @@ _SIGNALS = ([signal.SIGTERM] if os.name == "nt" else class ErrorHandler(object): """Registers and calls cleanup functions in case of an error.""" def __init__(self, func=None): - self.funcs = [func] if func else [] + self.funcs = [] self.prev_handlers = {} + if func: + self.register(func) def __enter__(self): self.set_signal_handlers() diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index af145ab0a..a0d2eb97f 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -321,7 +321,7 @@ class IInstaller(IPlugin): """ - def recovery_routine(self): + def recovery_routine(): """Revert configuration to most recent finalized checkpoint. Remove all changes (temporary and permanent) that have not been diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 93fdf2cd3..0131d3c93 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -178,6 +178,39 @@ class ClientTest(unittest.TestCase): shutil.rmtree(tmp_path) + def test_deploy_certificate(self): + self.assertRaises(errors.Error, self.client.deploy_certificate, + ["foo.bar"], "key", "cert", "chain") + + installer = mock.MagicMock() + self.client.installer = installer + + self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain") + installer.deploy_cert.assert_called_once_with( + "foo.bar", os.path.abspath("cert"), + os.path.abspath("key"), os.path.abspath("chain")) + self.assertTrue(installer.save.call_count == 1) + installer.restart.assert_called_once_with() + + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config(self, mock_enhancements): + self.assertRaises(errors.Error, + self.client.enhance_config, ["foo.bar"]) + + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + + self.client.enhance_config(["foo.bar"]) + installer.enhance.assert_called_once_with("foo.bar", "redirect") + self.assertTrue(installer.save.call_count == 1) + installer.restart.assert_called_once_with() + + installer.enhance.side_effect = errors.PluginError + self.assertRaises(errors.PluginError, + self.client.enhance_config, ["foo.bar"], True) + installer.recovery_routine.assert_called_once_with() + class RollbackTest(unittest.TestCase): """Tests for letsencrypt.client.rollback.""" diff --git a/letsencrypt/tests/error_handler_test.py b/letsencrypt/tests/error_handler_test.py index 6c6d02ec3..6927b32a0 100644 --- a/letsencrypt/tests/error_handler_test.py +++ b/letsencrypt/tests/error_handler_test.py @@ -1,25 +1,46 @@ """Tests for letsencrypt.error_handler.""" +import signal import unittest import mock +from letsencrypt import error_handler + class ErrorHandlerTest(unittest.TestCase): """Tests for letsencrypt.error_handler.""" def setUp(self): - from letsencrypt import error_handler self.init_func = mock.MagicMock() - self.error_handler = error_handler.ErrorHandler(self.init_func) + self.handler = error_handler.ErrorHandler(self.init_func) def test_context_manager(self): try: - with self.error_handler: + with self.handler: raise ValueError except ValueError: pass self.init_func.assert_called_once_with() + @mock.patch('letsencrypt.error_handler.os') + @mock.patch('letsencrypt.error_handler.signal') + def test_signal_handler(self, mock_signal, mock_os): + # pylint: disable=protected-access + mock_signal.getsignal.return_value = signal.SIG_DFL + self.handler.set_signal_handlers() + signal_handler = self.handler._signal_handler + for signum in error_handler._SIGNALS: + mock_signal.signal.assert_any_call(signum, signal_handler) + + signum = error_handler._SIGNALS[0] + signal_handler(signum, None) + self.init_func.assert_called_once_with() + mock_os.kill.assert_called_once_with(mock_os.getpid(), signum) + + self.handler.reset_signal_handlers() + for signum in error_handler._SIGNALS: + mock_signal.signal.assert_any_call(signum, signal.SIG_DFL) + if __name__ == "__main__": unittest.main() # pragma: no cover From 31e9519ef5af39550cd1d333d6c1ecd608f24221 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Sep 2015 15:11:10 -0700 Subject: [PATCH 018/145] Updated null installer interface --- letsencrypt/plugins/null.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/letsencrypt/plugins/null.py b/letsencrypt/plugins/null.py index bc9565e5a..efe041cac 100644 --- a/letsencrypt/plugins/null.py +++ b/letsencrypt/plugins/null.py @@ -47,6 +47,9 @@ class Installer(common.Plugin): def rollback_checkpoints(self, rollback=1): pass # pragma: no cover + def recovery_routine(self): + pass # pragma: no cover + def view_config_changes(self): pass # pragma: no cover From fd0c51e48afef3fb618d5027d4420a921c00f9a3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 24 Sep 2015 16:23:40 -0700 Subject: [PATCH 019/145] Incorporated Kuba's feedback and better defined corner cases --- letsencrypt/auth_handler.py | 14 ++++--- letsencrypt/client.py | 7 +++- letsencrypt/error_handler.py | 55 +++++++++++++++++++++---- letsencrypt/tests/client_test.py | 4 +- letsencrypt/tests/error_handler_test.py | 19 ++++++--- 5 files changed, 77 insertions(+), 22 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index a285825dc..68aed510a 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -107,12 +107,16 @@ class AuthHandler(object): """Get Responses for challenges from authenticators.""" cont_resp = [] dv_resp = [] - logger.info("Attempting to set up challenges.") with error_handler.ErrorHandler(self._cleanup_challenges): - if self.cont_c: - cont_resp = self.cont_auth.perform(self.cont_c) - if self.dv_c: - dv_resp = self.dv_auth.perform(self.dv_c) + try: + if self.cont_c: + cont_resp = self.cont_auth.perform(self.cont_c) + if self.dv_c: + dv_resp = self.dv_auth.perform(self.dv_c) + except errors.AuthorizationError: + logger.critical("Failure in setting up challenges.") + logger.info("Attempting to clean up outstanding challenges...") + raise assert len(cont_resp) == len(self.cont_c) assert len(dv_resp) == len(self.dv_c) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 3f1f4900b..56d9b1fda 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -415,8 +415,11 @@ class Client(object): """ with error_handler.ErrorHandler(self.installer.recovery_routine): for dom in domains: - logger.info("Attempting to perform redirect for %s", dom) - self.installer.enhance(dom, "redirect") + try: + self.installer.enhance(dom, "redirect") + except errors.PluginError: + logger.warn("Unable to perform redirect for %s", dom) + raise self.installer.save("Add Redirects") self.installer.restart() diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py index 3fc948b54..fedb66c0e 100644 --- a/letsencrypt/error_handler.py +++ b/letsencrypt/error_handler.py @@ -1,26 +1,58 @@ -"""Registers and calls cleanup functions in case of an error.""" +"""Registers functions to be called if an exception or signal occurs.""" +import logging import os import signal +import traceback +logger = logging.getLogger(__name__) + + +# _SIGNALS stores the signals that will be handled by the ErrorHandler. These +# signals were chosen as their default handler terminates the process and could +# potentially occur from inside Python. Signals such as SIGILL were not +# included as they could be a sign of something devious and we should terminate +# immediately. _SIGNALS = ([signal.SIGTERM] if os.name == "nt" else [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, signal.SIGXCPU, signal.SIGXFSZ, signal.SIGPWR]) class ErrorHandler(object): - """Registers and calls cleanup functions in case of an error.""" + """Registers functions to be called if an exception or signal occurs. + + This class allows you to register functions that will be called when + an exception or signal is encountered. The class works best as a + context manager. For example: + + with ErrorHandler(cleanup_func): + do_something() + + If an exception is raised out of do_something, cleanup_func will be + called. The exception is not caught by the ErrorHandler. Similarly, + if a signal is encountered, cleanup_func is called followed by the + previously registered signal handler. + + Every registered function is attempted to be run to completion + exactly once. If a registered function raises an exception, it is + logged and the next function is called. If a (different) handled + signal occurs while calling a registered function, it is attempted + to be called again by the next signal handler. + + """ def __init__(self, func=None): self.funcs = [] self.prev_handlers = {} - if func: + if func is not None: self.register(func) def __enter__(self): self.set_signal_handlers() - def __exit__(self, exec_type, exec_value, traceback): + def __exit__(self, exec_type, exec_value, trace): if exec_value is not None: + logger.debug("Encountered exception:\n%s", "".join( + traceback.format_exception(exec_type, exec_value, trace))) self.call_registered() self.reset_signal_handlers() @@ -29,9 +61,15 @@ class ErrorHandler(object): self.funcs.append(func) def call_registered(self): - """Calls all functions in the order they were registered.""" - for func in self.funcs: - func() + """Calls all registered functions""" + logger.debug("Calling registered functions") + while self.funcs: + try: + self.funcs[-1]() + except Exception as error: # pylint: disable=broad-except + logger.error("Encountered exception during recovery") + logger.exception(error) + self.funcs.pop() def set_signal_handlers(self): """Sets signal handlers for signals in _SIGNALS.""" @@ -48,12 +86,13 @@ class ErrorHandler(object): signal.signal(signum, self.prev_handlers[signum]) self.prev_handlers.clear() - def _signal_handler(self, signum, _): + def _signal_handler(self, signum, unused_frame): """Calls registered functions and the previous signal handler. :param int signum: number of current signal """ + logger.debug("Singal %s encountered", signum) self.call_registered() signal.signal(signum, self.prev_handlers[signum]) os.kill(os.getpid(), signum) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 0131d3c93..83cd54226 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -189,7 +189,7 @@ class ClientTest(unittest.TestCase): installer.deploy_cert.assert_called_once_with( "foo.bar", os.path.abspath("cert"), os.path.abspath("key"), os.path.abspath("chain")) - self.assertTrue(installer.save.call_count == 1) + self.assertEqual(installer.save.call_count, 1) installer.restart.assert_called_once_with() @mock.patch("letsencrypt.client.enhancements") @@ -203,7 +203,7 @@ class ClientTest(unittest.TestCase): self.client.enhance_config(["foo.bar"]) installer.enhance.assert_called_once_with("foo.bar", "redirect") - self.assertTrue(installer.save.call_count == 1) + self.assertEqual(installer.save.call_count, 1) installer.restart.assert_called_once_with() installer.enhance.side_effect = errors.PluginError diff --git a/letsencrypt/tests/error_handler_test.py b/letsencrypt/tests/error_handler_test.py index 6927b32a0..66acac930 100644 --- a/letsencrypt/tests/error_handler_test.py +++ b/letsencrypt/tests/error_handler_test.py @@ -4,15 +4,17 @@ import unittest import mock -from letsencrypt import error_handler - class ErrorHandlerTest(unittest.TestCase): """Tests for letsencrypt.error_handler.""" def setUp(self): + from letsencrypt import error_handler + self.init_func = mock.MagicMock() self.handler = error_handler.ErrorHandler(self.init_func) + # pylint: disable=protected-access + self.signals = error_handler._SIGNALS def test_context_manager(self): try: @@ -29,18 +31,25 @@ class ErrorHandlerTest(unittest.TestCase): mock_signal.getsignal.return_value = signal.SIG_DFL self.handler.set_signal_handlers() signal_handler = self.handler._signal_handler - for signum in error_handler._SIGNALS: + for signum in self.signals: mock_signal.signal.assert_any_call(signum, signal_handler) - signum = error_handler._SIGNALS[0] + signum = self.signals[0] signal_handler(signum, None) self.init_func.assert_called_once_with() mock_os.kill.assert_called_once_with(mock_os.getpid(), signum) self.handler.reset_signal_handlers() - for signum in error_handler._SIGNALS: + for signum in self.signals: mock_signal.signal.assert_any_call(signum, signal.SIG_DFL) + def test_bad_recovery(self): + bad_func = mock.MagicMock(side_effect=[ValueError]) + self.handler.register(bad_func) + self.handler.call_registered() + self.init_func.assert_called_once_with() + bad_func.assert_called_once_with() + if __name__ == "__main__": unittest.main() # pragma: no cover From fe810020c490d2c1bd8b32f806d28d80ad537ab1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 25 Sep 2015 13:26:45 -0700 Subject: [PATCH 020/145] Made error logging entries red in the terminal --- letsencrypt/cli.py | 3 +- letsencrypt/colored_logging.py | 47 +++++++++++++++++++++++ letsencrypt/le_util.py | 4 ++ letsencrypt/reporter.py | 6 +-- letsencrypt/tests/colored_logging_test.py | 39 +++++++++++++++++++ 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 letsencrypt/colored_logging.py create mode 100644 letsencrypt/tests/colored_logging_test.py diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e4787a849..7cb4a0458 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -24,6 +24,7 @@ from acme import jose import letsencrypt from letsencrypt import account +from letsencrypt import colored_logging from letsencrypt import configuration from letsencrypt import constants from letsencrypt import client @@ -786,7 +787,7 @@ def _setup_logging(args): level = -args.verbose_count * 10 fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" if args.text_mode: - handler = logging.StreamHandler() + handler = colored_logging.StreamHandler() handler.setFormatter(logging.Formatter(fmt)) else: handler = log.DialogHandler() diff --git a/letsencrypt/colored_logging.py b/letsencrypt/colored_logging.py new file mode 100644 index 000000000..89239e2c7 --- /dev/null +++ b/letsencrypt/colored_logging.py @@ -0,0 +1,47 @@ +"""A formatter and StreamHandler for colorizing logging output.""" +import logging +import sys + +from letsencrypt import le_util + + +class StreamHandler(logging.StreamHandler): + """Sends colored logging output to a stream. + + If the specified stream is not a tty, the class works like the + standard logging.StreamHandler. Default red_level is logging.WARNING. + + :ivar bool colored: True if output should be colored + :ivar bool red_level: The level at which to output + + """ + _RED = '\033[31m' + + def __init__(self, stream=None): + super(StreamHandler, self).__init__(stream) + self.colored = (sys.stderr.isatty() if stream is None else + stream.isatty()) + self.set_red_level(logging.WARNING) + + def format(self, record): + """Formats the string representation of record. + + :param logging.LogRecord record: Record to be formatted + + :returns: Formatted, string representation of record + :rtype: str + + """ + output = super(StreamHandler, self).format(record) + if self.colored and record.levelno >= self.red_level: + return ''.join((self._RED, output, le_util.ANSI_SGR_RESET)) + else: + return output + + def set_red_level(self, red_level): + """Sets the level necessary to display output in red. + + :param int red_level: Minimum log level for displaying red text + + """ + self.red_level = red_level diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index ffc7da190..74e03d8a1 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -18,6 +18,10 @@ Key = collections.namedtuple("Key", "file pem") CSR = collections.namedtuple("CSR", "file data form") +# ANSI escape code for resetting output format +ANSI_SGR_RESET = "\033[0m" + + def run_script(params): """Run the script with the given params. diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 0c4a7b378..86413053e 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -9,6 +9,7 @@ import textwrap import zope.interface from letsencrypt import interfaces +from letsencrypt import le_util logger = logging.getLogger(__name__) @@ -30,7 +31,6 @@ class Reporter(object): LOW_PRIORITY = 2 """Low priority constant. See `add_message`.""" - _RESET = '\033[0m' _BOLD = '\033[1m' _msg_type = collections.namedtuple('ReporterMsg', 'priority text on_crash') @@ -87,7 +87,7 @@ class Reporter(object): msg = self.messages.get() if no_exception or msg.on_crash: if bold_on and msg.priority > self.HIGH_PRIORITY: - sys.stdout.write(self._RESET) + sys.stdout.write(le_util.ANSI_SGR_RESET) bold_on = False lines = msg.text.splitlines() print first_wrapper.fill(lines[0]) @@ -95,4 +95,4 @@ class Reporter(object): print "\n".join( next_wrapper.fill(line) for line in lines[1:]) if bold_on: - sys.stdout.write(self._RESET) + sys.stdout.write(le_util.ANSI_SGR_RESET) diff --git a/letsencrypt/tests/colored_logging_test.py b/letsencrypt/tests/colored_logging_test.py new file mode 100644 index 000000000..fc97b2a49 --- /dev/null +++ b/letsencrypt/tests/colored_logging_test.py @@ -0,0 +1,39 @@ +"""Tests for letsencrypt.colored_logging.""" +import logging +import StringIO +import unittest + +from letsencrypt import le_util + + +class StreamHandlerTest(unittest.TestCase): + + def setUp(self): + from letsencrypt import colored_logging + + self.stream = StringIO.StringIO() + self.stream.isatty = lambda: True + self.handler = colored_logging.StreamHandler(self.stream) + + self.logger = logging.getLogger() + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(self.handler) + + def test_format(self): + msg = 'I did a thing' + self.logger.debug(msg) + self.assertEqual(self.stream.getvalue(), '{0}\n'.format(msg)) + + def test_format_and_red_level(self): + msg = 'I did another thing' + self.handler.set_red_level(logging.DEBUG) + self.logger.debug(msg) + + # pylint: disable=protected-access + expected = '{0}{1}{2}\n'.format(self.handler._RED, msg, + le_util.ANSI_SGR_RESET) + self.assertEqual(self.stream.getvalue(), expected) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From 817aadae6abd2c8e7ed4c3d038ba7cfe18f93be6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 25 Sep 2015 13:27:19 -0700 Subject: [PATCH 021/145] Fixed indentation --- letsencrypt/colored_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/colored_logging.py b/letsencrypt/colored_logging.py index 89239e2c7..f5750870e 100644 --- a/letsencrypt/colored_logging.py +++ b/letsencrypt/colored_logging.py @@ -26,7 +26,7 @@ class StreamHandler(logging.StreamHandler): def format(self, record): """Formats the string representation of record. - :param logging.LogRecord record: Record to be formatted + :param logging.LogRecord record: Record to be formatted :returns: Formatted, string representation of record :rtype: str From cfe103b4edc5e8366cccb7e34e1a890fe8ad9bfc Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 25 Sep 2015 20:01:12 -0700 Subject: [PATCH 022/145] unify quotes --- letsencrypt/tests/configuration_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 498147c6d..9692f9479 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -59,9 +59,9 @@ class RenewerConfigurationTest(unittest.TestCase): @mock.patch('letsencrypt.configuration.constants') def test_dynamic_dirs(self, constants): - constants.ARCHIVE_DIR = "a" + constants.ARCHIVE_DIR = 'a' constants.LIVE_DIR = 'l' - constants.RENEWAL_CONFIGS_DIR = "renewal_configs" + constants.RENEWAL_CONFIGS_DIR = 'renewal_configs' constants.RENEWER_CONFIG_FILENAME = 'r.conf' self.assertEqual(self.config.archive_dir, '/tmp/config/a') From add23360a560891431013766717d4bbe2af34688 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 25 Sep 2015 20:04:34 -0700 Subject: [PATCH 023/145] Take away confirmation screen for testing --- letsencrypt/cli.py | 6 +++--- tests/integration/_common.sh | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 88266aaeb..6e2849466 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -241,8 +241,8 @@ def _treat_as_renewal(config, domains): # We aren't in a duplicative-names situation at all, so we don't # have to tell or ask the user anything about this. pass - elif zope.component.getUtility(interfaces.IDisplay).yesno( - question, "Replace", "Cancel"): + elif config.no_confirm or zope.component.getUtility( + interfaces.IDisplay).yesno(question, "Replace", "Cancel"): renewal = True else: reporter_util = zope.component.getUtility(interfaces.IReporter) @@ -661,7 +661,7 @@ def create_parser(plugins, args): help="show program's version number and exit") helpful.add( "automation", "--no-confirm", dest="no_confirm", action="store_true", - help="Turn off confirmation screens, currently used for --revoke") + help="Turn off confirmation screens, used for renewal screens") helpful.add( "automation", "--agree-eula", dest="eula", action="store_true", help="Agree to the Let's Encrypt Developer Preview EULA") diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index c8b142cf2..7897ff1b7 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -23,6 +23,7 @@ letsencrypt_test () { --agree-eula \ --agree-tos \ --email "" \ + --no-confirm \ --debug \ -vvvvvvv \ "$@" From 8bc260dd64fee5ca4c76b957d72c86d70350604e Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 25 Sep 2015 21:45:56 -0700 Subject: [PATCH 024/145] Fix crypto_util tests --- letsencrypt/tests/crypto_util_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index b248ffd8a..b4d2aa394 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -6,7 +6,9 @@ import unittest import OpenSSL import mock +import zope.component +from letsencrypt import interfaces from letsencrypt.tests import test_util @@ -20,6 +22,8 @@ class InitSaveKeyTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.init_save_key.""" def setUp(self): logging.disable(logging.CRITICAL) + zope.component.provideUtility( + mock.Mock(strict_permissions=True), interfaces.IConfig) self.key_dir = tempfile.mkdtemp('key_dir') def tearDown(self): @@ -48,6 +52,8 @@ class InitSaveCSRTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.init_save_csr.""" def setUp(self): + zope.component.provideUtility( + mock.Mock(strict_permissions=True), interfaces.IConfig) self.csr_dir = tempfile.mkdtemp('csr_dir') def tearDown(self): From b72f451a1b5056be4d22a32f5bb75f744ff21a33 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 25 Sep 2015 22:26:32 -0700 Subject: [PATCH 025/145] rename certs directory to csr directory --- letsencrypt/client.py | 2 +- letsencrypt/configuration.py | 7 +++---- letsencrypt/constants.py | 4 ++-- letsencrypt/interfaces.py | 7 ++----- letsencrypt/tests/client_test.py | 2 +- letsencrypt/tests/configuration_test.py | 4 ++-- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 60eaea5a1..7f035dc25 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -211,7 +211,7 @@ class Client(object): # Create CSR from names key = crypto_util.init_save_key( self.config.rsa_key_size, self.config.key_dir) - csr = crypto_util.init_save_csr(key, domains, self.config.cert_dir) + csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) return self._obtain_certificate(domains, csr) + (key, csr) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 6f3ece9fd..bd1ba162a 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -18,8 +18,7 @@ class NamespaceConfig(object): paths defined in :py:mod:`letsencrypt.constants`: - `accounts_dir` - - `cert_dir` - - `cert_key_backup` + - `csr_dir` - `in_progress_dir` - `key_dir` - `renewer_config_file` @@ -54,8 +53,8 @@ class NamespaceConfig(object): return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR) @property - def cert_dir(self): # pylint: disable=missing-docstring - return os.path.join(self.namespace.config_dir, constants.CERT_DIR) + def csr_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.CSR_DIR) @property def cert_key_backup(self): # pylint: disable=missing-docstring diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 6c67ce445..0456d3253 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -68,8 +68,8 @@ ACCOUNTS_DIR = "accounts" BACKUP_DIR = "backups" """Directory (relative to `IConfig.work_dir`) where backups are kept.""" -CERT_DIR = "certs" -"""See `.IConfig.cert_dir`.""" +CSR_DIR = "csr" +"""See `.IConfig.csr_dir`.""" CERT_KEY_BACKUP_DIR = "keys-certs" """Directory where all certificates and keys are stored (relative to diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 5db92b368..139e2e9f4 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -205,12 +205,9 @@ class IConfig(zope.interface.Interface): accounts_dir = zope.interface.Attribute( "Directory where all account information is stored.") backup_dir = zope.interface.Attribute("Configuration backups directory.") - cert_dir = zope.interface.Attribute( + csr_dir = zope.interface.Attribute( "Directory where newly generated Certificate Signing Requests " - "(CSRs) and certificates not enrolled in the renewer are saved.") - cert_key_backup = zope.interface.Attribute( - "Directory where all certificates and keys are stored. " - "Used for easy revocation.") + "(CSRs) are saved.") in_progress_dir = zope.interface.Attribute( "Directory used before a permanent checkpoint is finalized.") key_dir = zope.interface.Attribute("Keys storage.") diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 93fdf2cd3..fe1cb1243 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -113,7 +113,7 @@ class ClientTest(unittest.TestCase): mock_crypto_util.init_save_key.assert_called_once_with( self.config.rsa_key_size, self.config.key_dir) mock_crypto_util.init_save_csr.assert_called_once_with( - mock.sentinel.key, domains, self.config.cert_dir) + mock.sentinel.key, domains, self.config.csr_dir) self._check_obtain_certificate() @mock.patch("letsencrypt.client.zope.component.getUtility") diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 498147c6d..79f867be9 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -33,7 +33,7 @@ class NamespaceConfigTest(unittest.TestCase): constants.ACCOUNTS_DIR = 'acc' constants.BACKUP_DIR = 'backups' constants.CERT_KEY_BACKUP_DIR = 'c/' - constants.CERT_DIR = 'certs' + constants.CSR_DIR = 'csr' constants.IN_PROGRESS_DIR = '../p' constants.KEY_DIR = 'keys' constants.TEMP_CHECKPOINT_DIR = 't' @@ -41,7 +41,7 @@ class NamespaceConfigTest(unittest.TestCase): self.assertEqual( self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new') self.assertEqual(self.config.backup_dir, '/tmp/foo/backups') - self.assertEqual(self.config.cert_dir, '/tmp/config/certs') + self.assertEqual(self.config.csr_dir, '/tmp/config/csr') self.assertEqual( self.config.cert_key_backup, '/tmp/foo/c/acme-server.org:443/new') self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') From 022c5c3c243c321b7e2956876ebc95f8e2d0af75 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 25 Sep 2015 22:35:43 -0700 Subject: [PATCH 026/145] Remove revoker and associated code --- letsencrypt/configuration.py | 6 - letsencrypt/constants.py | 4 - letsencrypt/interfaces.py | 3 - letsencrypt/revoker.py | 560 ------------------------ letsencrypt/tests/configuration_test.py | 3 - letsencrypt/tests/revoker_test.py | 409 ----------------- 6 files changed, 985 deletions(-) delete mode 100644 letsencrypt/revoker.py delete mode 100644 letsencrypt/tests/revoker_test.py diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 6f3ece9fd..ec8ddb14e 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -19,7 +19,6 @@ class NamespaceConfig(object): - `accounts_dir` - `cert_dir` - - `cert_key_backup` - `in_progress_dir` - `key_dir` - `renewer_config_file` @@ -57,11 +56,6 @@ class NamespaceConfig(object): def cert_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.config_dir, constants.CERT_DIR) - @property - def cert_key_backup(self): # pylint: disable=missing-docstring - return os.path.join(self.namespace.work_dir, - constants.CERT_KEY_BACKUP_DIR, self.server_path) - @property def in_progress_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 6c67ce445..adca4ed02 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -71,10 +71,6 @@ BACKUP_DIR = "backups" CERT_DIR = "certs" """See `.IConfig.cert_dir`.""" -CERT_KEY_BACKUP_DIR = "keys-certs" -"""Directory where all certificates and keys are stored (relative to -`IConfig.work_dir`). Used for easy revocation.""" - IN_PROGRESS_DIR = "IN_PROGRESS" """Directory used before a permanent checkpoint is finalized (relative to `IConfig.work_dir`).""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 5db92b368..345a0d779 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -208,9 +208,6 @@ class IConfig(zope.interface.Interface): cert_dir = zope.interface.Attribute( "Directory where newly generated Certificate Signing Requests " "(CSRs) and certificates not enrolled in the renewer are saved.") - cert_key_backup = zope.interface.Attribute( - "Directory where all certificates and keys are stored. " - "Used for easy revocation.") in_progress_dir = zope.interface.Attribute( "Directory used before a permanent checkpoint is finalized.") key_dir = zope.interface.Attribute("Keys storage.") diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py deleted file mode 100644 index 32c6f003d..000000000 --- a/letsencrypt/revoker.py +++ /dev/null @@ -1,560 +0,0 @@ -"""Revoker module to enable LE revocations. - -The backend of this module would fit a database quite nicely, but in order to -minimize dependencies and maintain transparency, the class currently implements -its own storage system. The number of certs that will likely be stored on any -given client might not warrant requiring a database. - -""" -import collections -import csv -import logging -import os -import shutil -import tempfile - -import OpenSSL - -from acme import client as acme_client -from acme import crypto_util as acme_crypto_util -from acme.jose import util as jose_util - -from letsencrypt import crypto_util -from letsencrypt import errors -from letsencrypt import le_util - -from letsencrypt.display import util as display_util -from letsencrypt.display import revocation - - -logger = logging.getLogger(__name__) - - -class Revoker(object): - """A revocation class for LE. - - .. todo:: Add a method to specify your own certificate for revocation - CLI - - :ivar .acme.client.Client acme: ACME client - - :ivar installer: Installer object - :type installer: :class:`~letsencrypt.interfaces.IInstaller` - - :ivar config: Configuration. - :type config: :class:`~letsencrypt.interfaces.IConfig` - - :ivar bool no_confirm: Whether or not to ask for confirmation for revocation - - """ - def __init__(self, installer, config, no_confirm=False): - # XXX - self.acme = acme_client.Client(directory=None, key=None, alg=None) - - self.installer = installer - self.config = config - self.no_confirm = no_confirm - - le_util.make_or_verify_dir(config.cert_key_backup, 0o700, os.geteuid(), - self.config.strict_permissions) - - # TODO: Find a better solution for this... - self.list_path = os.path.join(config.cert_key_backup, "LIST") - # Make sure that the file is available for use for rest of class - open(self.list_path, "a").close() - - def revoke_from_key(self, authkey): - """Revoke all certificates under an authorized key. - - :param authkey: Authorized key used in previous transactions - :type authkey: :class:`letsencrypt.le_util.Key` - - """ - certs = [] - try: - clean_pem = OpenSSL.crypto.dump_privatekey( - OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, authkey.pem)) - except OpenSSL.crypto.Error as error: - logger.debug(error, exc_info=True) - raise errors.RevokerError( - "Invalid key file specified to revoke_from_key") - - with open(self.list_path, "rb") as csvfile: - csvreader = csv.reader(csvfile) - for row in csvreader: - # idx, cert, key - # Add all keys that match to marked list - # Note: The key can be different than the pub key found in the - # certificate. - _, b_k = self._row_to_backup(row) - try: - test_pem = OpenSSL.crypto.dump_privatekey( - OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, open(b_k).read())) - except OpenSSL.crypto.Error as error: - logger.debug(error, exc_info=True) - # This should never happen given the assumptions of the - # module. If it does, it is probably best to delete the - # the offending key/cert. For now... just raise an exception - raise errors.RevokerError("%s - backup file is corrupted.") - - if clean_pem == test_pem: - certs.append( - Cert.fromrow(row, self.config.cert_key_backup)) - if certs: - self._safe_revoke(certs) - else: - logger.info("No certificates using the authorized key were found.") - - def revoke_from_cert(self, cert_path): - """Revoke a certificate by specifying a file path. - - .. todo:: Add the ability to revoke the certificate even if the cert - is not stored locally. A path to the auth key will need to be - attained from the user. - - :param str cert_path: path to ACME certificate in pem form - - """ - # Locate the correct certificate (do not rely on filename) - cert_to_revoke = Cert(cert_path) - - with open(self.list_path, "rb") as csvfile: - csvreader = csv.reader(csvfile) - for row in csvreader: - cert = Cert.fromrow(row, self.config.cert_key_backup) - - if cert.get_der() == cert_to_revoke.get_der(): - self._safe_revoke([cert]) - return - - logger.info("Associated ACME certificate was not found.") - - def revoke_from_menu(self): - """List trusted Let's Encrypt certificates.""" - - csha1_vhlist = self._get_installed_locations() - certs = self._populate_saved_certs(csha1_vhlist) - - while True: - if certs: - code, selection = revocation.display_certs(certs) - - if code == display_util.OK: - revoked_certs = self._safe_revoke([certs[selection]]) - # Since we are currently only revoking one cert at a time... - if revoked_certs: - del certs[selection] - elif code == display_util.HELP: - revocation.more_info_cert(certs[selection]) - else: - return - else: - logger.info( - "There are not any trusted Let's Encrypt " - "certificates for this server.") - return - - def _populate_saved_certs(self, csha1_vhlist): - # pylint: disable=no-self-use - """Populate a list of all the saved certs. - - It is important to read from the file rather than the directory. - We assume that the LIST file is the master record and depending on - program crashes, this may differ from what is actually in the directory. - Namely, additional certs/keys may exist. There should never be any - certs/keys in the LIST that don't exist in the directory however. - - :param dict csha1_vhlist: map from cert sha1 fingerprints to a list - of it's installed location paths. - - """ - certs = [] - with open(self.list_path, "rb") as csvfile: - csvreader = csv.reader(csvfile) - # idx, orig_cert, orig_key - for row in csvreader: - cert = Cert.fromrow(row, self.config.cert_key_backup) - - # If we were able to find the cert installed... update status - cert.installed = csha1_vhlist.get(cert.get_fingerprint(), []) - - certs.append(cert) - - return certs - - def _get_installed_locations(self): - """Get installed locations of certificates. - - :returns: map from cert sha1 fingerprint to :class:`list` of vhosts - where the certificate is installed. - - """ - csha1_vhlist = {} - - if self.installer is None: - return csha1_vhlist - - for (cert_path, _, path) in self.installer.get_all_certs_keys(): - try: - with open(cert_path) as cert_file: - cert_data = cert_file.read() - except IOError: - continue - try: - cert_obj, _ = crypto_util.pyopenssl_load_certificate(cert_data) - except errors.Error: - continue - cert_sha1 = cert_obj.digest("sha1") - if cert_sha1 in csha1_vhlist: - csha1_vhlist[cert_sha1].append(path) - else: - csha1_vhlist[cert_sha1] = [path] - - return csha1_vhlist - - def _safe_revoke(self, certs): - """Confirm and revoke certificates. - - :param certs: certs intended to be revoked - :type certs: :class:`list` of :class:`letsencrypt.revoker.Cert` - - :returns: certs successfully revoked - :rtype: :class:`list` of :class:`letsencrypt.revoker.Cert` - - """ - success_list = [] - try: - for cert in certs: - if self.no_confirm or revocation.confirm_revocation(cert): - try: - self._acme_revoke(cert) - except errors.Error: - # TODO: Improve error handling when networking is set... - logger.error( - "Unable to revoke cert:%s%s", os.linesep, str(cert)) - success_list.append(cert) - revocation.success_revocation(cert) - finally: - if success_list: - self._remove_certs_keys(success_list) - - return success_list - - def _acme_revoke(self, cert): - """Revoke the certificate with the ACME server. - - :param cert: certificate to revoke - :type cert: :class:`letsencrypt.revoker.Cert` - - :returns: TODO - - """ - # XXX | pylint: disable=unused-variable - - # pylint: disable=protected-access - certificate = jose_util.ComparableX509(cert._cert) - try: - with open(cert.backup_key_path, "rU") as backup_key_file: - key = OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, backup_key_file.read()) - # If the key file doesn't exist... or is corrupted - except OpenSSL.crypto.Error as error: - logger.debug(error, exc_info=True) - raise errors.RevokerError( - "Corrupted backup key file: %s" % cert.backup_key_path) - - return self.acme.revoke(cert=None) # XXX - - def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use - """Remove certificate and key. - - :param list cert_list: Must contain certs, each is of type - :class:`letsencrypt.revoker.Cert` - - """ - # This must occur first, LIST is the official key - self._remove_certs_from_list(cert_list) - - # Remove files - for cert in cert_list: - os.remove(cert.backup_path) - os.remove(cert.backup_key_path) - - def _remove_certs_from_list(self, cert_list): # pylint: disable=no-self-use - """Remove a certificate from the LIST file. - - :param list cert_list: Must contain valid certs, each is of type - :class:`letsencrypt.revoker.Cert` - - """ - newfile_handle, list_path2 = tempfile.mkstemp(".tmp", "LIST") - idx = 0 - - with open(self.list_path, "rb") as orgfile: - csvreader = csv.reader(orgfile) - with os.fdopen(newfile_handle, "wb") as newfile: - csvwriter = csv.writer(newfile) - - for row in csvreader: - if idx >= len(cert_list) or row != cert_list[idx].get_row(): - csvwriter.writerow(row) - else: - idx += 1 - - # This should never happen... - if idx != len(cert_list): - raise errors.RevokerError( - "Did not find all cert_list items to remove from LIST") - - shutil.copy2(list_path2, self.list_path) - os.remove(list_path2) - - def _row_to_backup(self, row): - """Convenience function - - :param list row: csv file row 'idx', 'cert_path', 'key_path' - - :returns: tuple of the form ('backup_cert_path', 'backup_key_path') - :rtype: tuple - - """ - return (self._get_backup(self.config.cert_key_backup, row[0], row[1]), - self._get_backup(self.config.cert_key_backup, row[0], row[2])) - - @classmethod - def store_cert_key(cls, cert_path, key_path, config): - """Store certificate key. (Used to allow quick revocation) - - :param str cert_path: Path to a certificate file. - :param str key_path: Path to authorized key for certificate - - :ivar config: Configuration. - :type config: :class:`~letsencrypt.interfaces.IConfig` - - """ - list_path = os.path.join(config.cert_key_backup, "LIST") - le_util.make_or_verify_dir(config.cert_key_backup, 0o700, os.geteuid(), - config.strict_permissions) - - cls._catalog_files( - config.cert_key_backup, cert_path, key_path, list_path) - - @classmethod - def _catalog_files(cls, backup_dir, cert_path, key_path, list_path): - idx = 0 - if os.path.isfile(list_path): - with open(list_path, "r+b") as csvfile: - csvreader = csv.reader(csvfile) - - # Find the highest index in the file - for row in csvreader: - idx = int(row[0]) + 1 - csvwriter = csv.writer(csvfile) - # You must move the files before appending the row - cls._copy_files(backup_dir, idx, cert_path, key_path) - csvwriter.writerow([str(idx), cert_path, key_path]) - - else: - with open(list_path, "wb") as csvfile: - csvwriter = csv.writer(csvfile) - # You must move the files before appending the row - cls._copy_files(backup_dir, idx, cert_path, key_path) - csvwriter.writerow([str(idx), cert_path, key_path]) - - @classmethod - def _copy_files(cls, backup_dir, idx, cert_path, key_path): - """Copies the files into the backup dir appropriately.""" - shutil.copy2(cert_path, cls._get_backup(backup_dir, idx, cert_path)) - shutil.copy2(key_path, cls._get_backup(backup_dir, idx, key_path)) - - @classmethod - def _get_backup(cls, backup_dir, idx, orig_path): - """Returns the path to the backup.""" - return os.path.join( - backup_dir, "{name}_{idx}".format( - name=os.path.basename(orig_path), idx=str(idx))) - - -class Cert(object): - """Cert object used for Revocation convenience. - - :ivar _cert: Certificate - :type _cert: :class:`OpenSSL.crypto.X509` - - :ivar int idx: convenience index used for listing - :ivar orig: (`str` path - original certificate, `str` status) - :type orig: :class:`PathStatus` - :ivar orig_key: (`str` path - original auth key, `str` status) - :type orig_key: :class:`PathStatus` - :ivar str backup_path: backup filepath of the certificate - :ivar str backup_key_path: backup filepath of the authorized key - - :ivar list installed: `list` of `str` describing all locations the cert - is installed - - """ - PathStatus = collections.namedtuple("PathStatus", "path status") - """Convenience container to hold path and status info""" - - DELETED_MSG = "This file has been moved or deleted" - CHANGED_MSG = "This file has changed" - - def __init__(self, cert_path): - """Cert initialization - - :param str cert_filepath: Name of file containing certificate in - PEM format. - - """ - try: - with open(cert_path) as cert_file: - cert_data = cert_file.read() - except IOError: - raise errors.RevokerError( - "Error loading certificate: %s" % cert_path) - - try: - self._cert = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, cert_data) - except OpenSSL.crypto.Error: - raise errors.RevokerError( - "Error loading certificate: %s" % cert_path) - - self.idx = -1 - - self.orig = None - self.orig_key = None - self.backup_path = "" - self.backup_key_path = "" - - self.installed = ["Unknown"] - - @classmethod - def fromrow(cls, row, backup_dir): - # pylint: disable=protected-access - """Initialize Cert from a csv row.""" - idx = int(row[0]) - backup = Revoker._get_backup(backup_dir, idx, row[1]) - backup_key = Revoker._get_backup(backup_dir, idx, row[2]) - - obj = cls(backup) - obj.add_meta(idx, row[1], row[2], backup, backup_key) - return obj - - def get_row(self): - """Returns a list in CSV format. If meta data is available.""" - if self.orig is not None and self.orig_key is not None: - return [str(self.idx), self.orig.path, self.orig_key.path] - return None - - def add_meta(self, idx, orig, orig_key, backup, backup_key): - """Add meta data to cert - - :param int idx: convenience index for revoker - :param tuple orig: (`str` original certificate filepath, `str` status) - :param tuple orig_key: (`str` original auth key path, `str` status) - :param str backup: backup certificate filepath - :param str backup_key: backup key filepath - - """ - status = "" - key_status = "" - - # Verify original cert path - if not os.path.isfile(orig): - status = Cert.DELETED_MSG - else: - with open(orig) as orig_file: - orig_data = orig_file.read() - o_cert = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, orig_data) - if self.get_fingerprint() != o_cert.digest("sha1"): - status = Cert.CHANGED_MSG - - # Verify original key path - if not os.path.isfile(orig_key): - key_status = Cert.DELETED_MSG - else: - with open(orig_key, "r") as fd: - key_pem = fd.read() - with open(backup_key, "r") as fd: - backup_key_pem = fd.read() - if key_pem != backup_key_pem: - key_status = Cert.CHANGED_MSG - - self.idx = idx - self.orig = Cert.PathStatus(orig, status) - self.orig_key = Cert.PathStatus(orig_key, key_status) - self.backup_path = backup - self.backup_key_path = backup_key - - def get_cn(self): - """Get common name.""" - return self._cert.get_subject().CN - - def get_fingerprint(self): - """Get SHA1 fingerprint.""" - return self._cert.digest("sha1") - - def get_not_before(self): - """Get not_valid_before field.""" - return crypto_util.asn1_generalizedtime_to_dt( - self._cert.get_notBefore()) - - def get_not_after(self): - """Get not_valid_after field.""" - return crypto_util.asn1_generalizedtime_to_dt( - self._cert.get_notAfter()) - - def get_der(self): - """Get certificate in der format.""" - return OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, self._cert) - - def get_pub_key(self): - """Get public key size. - - .. todo:: Support for ECC - - """ - return "RSA {0}".format(self._cert.get_pubkey().bits) - - def get_san(self): - """Get subject alternative name if available.""" - # pylint: disable=protected-access - return ", ".join(acme_crypto_util._pyopenssl_cert_or_req_san(self._cert)) - - def __str__(self): - text = [ - "Subject: %s" % crypto_util.pyopenssl_x509_name_as_text( - self._cert.get_subject()), - "SAN: %s" % self.get_san(), - "Issuer: %s" % crypto_util.pyopenssl_x509_name_as_text( - self._cert.get_issuer()), - "Public Key: %s" % self.get_pub_key(), - "Not Before: %s" % str(self.get_not_before()), - "Not After: %s" % str(self.get_not_after()), - "Serial Number: %s" % self._cert.get_serial_number(), - "SHA1: %s%s" % (self.get_fingerprint(), os.linesep), - "Installed: %s" % ", ".join(self.installed), - ] - - if self.orig is not None: - if self.orig.status == "": - text.append("Path: %s" % self.orig.path) - else: - text.append("Orig Path: %s (%s)" % self.orig) - if self.orig_key is not None: - if self.orig_key.status == "": - text.append("Auth Key Path: %s" % self.orig_key.path) - else: - text.append("Orig Auth Key Path: %s (%s)" % self.orig_key) - - text.append("") - return os.linesep.join(text) - - def pretty_print(self): - """Nicely frames a cert str""" - frame = "-" * (display_util.WIDTH - 4) + os.linesep - return "{frame}{cert}{frame}".format(frame=frame, cert=str(self)) diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 498147c6d..110bfe223 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -32,7 +32,6 @@ class NamespaceConfigTest(unittest.TestCase): def test_dynamic_dirs(self, constants): constants.ACCOUNTS_DIR = 'acc' constants.BACKUP_DIR = 'backups' - constants.CERT_KEY_BACKUP_DIR = 'c/' constants.CERT_DIR = 'certs' constants.IN_PROGRESS_DIR = '../p' constants.KEY_DIR = 'keys' @@ -42,8 +41,6 @@ class NamespaceConfigTest(unittest.TestCase): self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new') self.assertEqual(self.config.backup_dir, '/tmp/foo/backups') self.assertEqual(self.config.cert_dir, '/tmp/config/certs') - self.assertEqual( - self.config.cert_key_backup, '/tmp/foo/c/acme-server.org:443/new') self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') self.assertEqual(self.config.key_dir, '/tmp/config/keys') self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py deleted file mode 100644 index 87dab4eb8..000000000 --- a/letsencrypt/tests/revoker_test.py +++ /dev/null @@ -1,409 +0,0 @@ -"""Test letsencrypt.revoker.""" -import csv -import os -import shutil -import tempfile -import unittest - -import mock -import OpenSSL - -from letsencrypt import errors -from letsencrypt import le_util -from letsencrypt.display import util as display_util - -from letsencrypt.tests import test_util - - -KEY = OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, test_util.load_vector("rsa512_key.pem")) - - -class RevokerBase(unittest.TestCase): # pylint: disable=too-few-public-methods - """Base Class for Revoker Tests.""" - def setUp(self): - self.paths, self.certs, self.key_path = create_revoker_certs() - - self.backup_dir = tempfile.mkdtemp("cert_backup") - self.mock_config = mock.MagicMock(cert_key_backup=self.backup_dir) - - self.list_path = os.path.join(self.backup_dir, "LIST") - - def _store_certs(self): - # pylint: disable=protected-access - from letsencrypt.revoker import Revoker - Revoker.store_cert_key(self.paths[0], self.key_path, self.mock_config) - Revoker.store_cert_key(self.paths[1], self.key_path, self.mock_config) - - # Set metadata - for i in xrange(2): - self.certs[i].add_meta( - i, self.paths[i], self.key_path, - Revoker._get_backup(self.backup_dir, i, self.paths[i]), - Revoker._get_backup(self.backup_dir, i, self.key_path)) - - def _get_rows(self): - with open(self.list_path, "rb") as csvfile: - return [row for row in csv.reader(csvfile)] - - def _write_rows(self, rows): - with open(self.list_path, "wb") as csvfile: - csvwriter = csv.writer(csvfile) - for row in rows: - csvwriter.writerow(row) - - -class RevokerTest(RevokerBase): - def setUp(self): - from letsencrypt.revoker import Revoker - super(RevokerTest, self).setUp() - - with open(self.key_path) as key_file: - self.key = le_util.Key(self.key_path, key_file.read()) - - self._store_certs() - - self.revoker = Revoker( - installer=mock.MagicMock(), config=self.mock_config) - - def tearDown(self): - shutil.rmtree(self.backup_dir) - - @mock.patch("acme.client.Client.revoke") - @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_key_all(self, mock_display, mock_acme): - mock_display().confirm_revocation.return_value = True - - self.revoker.revoke_from_key(self.key) - self.assertEqual(self._get_rows(), []) - - # Check to make sure backups were eliminated - for i in xrange(2): - self.assertFalse(self._backups_exist(self.certs[i].get_row())) - - self.assertEqual(mock_acme.call_count, 2) - - @mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_privatekey") - def test_revoke_by_invalid_keys(self, mock_load_privatekey): - mock_load_privatekey.side_effect = OpenSSL.crypto.Error - self.assertRaises( - errors.RevokerError, self.revoker.revoke_from_key, self.key) - - mock_load_privatekey.side_effect = [KEY, OpenSSL.crypto.Error] - self.assertRaises( - errors.RevokerError, self.revoker.revoke_from_key, self.key) - - @mock.patch("acme.client.Client.revoke") - @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_wrong_key(self, mock_display, mock_acme): - mock_display().confirm_revocation.return_value = True - - key_path = test_util.vector_path("rsa256_key.pem") - - wrong_key = le_util.Key(key_path, open(key_path).read()) - self.revoker.revoke_from_key(wrong_key) - - # Nothing was removed - self.assertEqual(len(self._get_rows()), 2) - # No revocation went through - self.assertEqual(mock_acme.call_count, 0) - - @mock.patch("acme.client.Client.revoke") - @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_cert(self, mock_display, mock_acme): - mock_display().confirm_revocation.return_value = True - - self.revoker.revoke_from_cert(self.paths[1]) - - row0 = self.certs[0].get_row() - row1 = self.certs[1].get_row() - - self.assertEqual(self._get_rows(), [row0]) - - self.assertTrue(self._backups_exist(row0)) - self.assertFalse(self._backups_exist(row1)) - - self.assertEqual(mock_acme.call_count, 1) - - @mock.patch("acme.client.Client.revoke") - @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_cert_not_found(self, mock_display, mock_acme): - mock_display().confirm_revocation.return_value = True - - self.revoker.revoke_from_cert(self.paths[0]) - self.revoker.revoke_from_cert(self.paths[0]) - - row0 = self.certs[0].get_row() - row1 = self.certs[1].get_row() - - # Same check as last time... just reversed. - self.assertEqual(self._get_rows(), [row1]) - - self.assertTrue(self._backups_exist(row1)) - self.assertFalse(self._backups_exist(row0)) - - self.assertEqual(mock_acme.call_count, 1) - - @mock.patch("acme.client.Client.revoke") - @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_menu(self, mock_display, mock_acme): - mock_display().confirm_revocation.return_value = True - mock_display.display_certs.side_effect = [ - (display_util.HELP, 0), - (display_util.OK, 0), - (display_util.CANCEL, -1), - ] - - self.revoker.revoke_from_menu() - - row0 = self.certs[0].get_row() - row1 = self.certs[1].get_row() - - self.assertEqual(self._get_rows(), [row1]) - - self.assertFalse(self._backups_exist(row0)) - self.assertTrue(self._backups_exist(row1)) - - self.assertEqual(mock_acme.call_count, 1) - self.assertEqual(mock_display.more_info_cert.call_count, 1) - - @mock.patch("letsencrypt.revoker.logger") - @mock.patch("acme.client.Client.revoke") - @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_menu_delete_all(self, mock_display, mock_acme, mock_log): - mock_display().confirm_revocation.return_value = True - mock_display.display_certs.return_value = (display_util.OK, 0) - - self.revoker.revoke_from_menu() - - self.assertEqual(self._get_rows(), []) - - # Everything should be deleted... - for i in xrange(2): - self.assertFalse(self._backups_exist(self.certs[i].get_row())) - - self.assertEqual(mock_acme.call_count, 2) - # Info is called when there aren't any certs left... - self.assertTrue(mock_log.info.called) - - @mock.patch("letsencrypt.revoker.revocation") - @mock.patch("letsencrypt.revoker.Revoker._acme_revoke") - @mock.patch("letsencrypt.revoker.logger") - def test_safe_revoke_acme_fail(self, mock_log, mock_revoke, mock_display): - # pylint: disable=protected-access - mock_revoke.side_effect = errors.Error - mock_display().confirm_revocation.return_value = True - - self.revoker._safe_revoke(self.certs) - self.assertTrue(mock_log.error.called) - - @mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_privatekey") - def test_acme_revoke_failure(self, mock_load_privatekey): - # pylint: disable=protected-access - mock_load_privatekey.side_effect = OpenSSL.crypto.Error - self.assertRaises( - errors.Error, self.revoker._acme_revoke, self.certs[0]) - - def test_remove_certs_from_list_bad_certs(self): - # pylint: disable=protected-access - from letsencrypt.revoker import Cert - - new_cert = Cert(self.paths[0]) - - # This isn't stored in the db - new_cert.idx = 10 - new_cert.backup_path = self.paths[0] - new_cert.backup_key_path = self.key_path - new_cert.orig = Cert.PathStatus("false path", "not here") - new_cert.orig_key = Cert.PathStatus("false path", "not here") - - self.assertRaises(errors.RevokerError, - self.revoker._remove_certs_from_list, [new_cert]) - - def _backups_exist(self, row): - # pylint: disable=protected-access - cert_path, key_path = self.revoker._row_to_backup(row) - return os.path.isfile(cert_path) and os.path.isfile(key_path) - - -class RevokerInstallerTest(RevokerBase): - def setUp(self): - super(RevokerInstallerTest, self).setUp() - - self.installs = [ - ["installation/path0a", "installation/path0b"], - ["installation/path1"], - ] - - self.certs_keys = [ - (self.paths[0], self.key_path, self.installs[0][0]), - (self.paths[0], self.key_path, self.installs[0][1]), - (self.paths[1], self.key_path, self.installs[1][0]), - ] - - self._store_certs() - - def _get_revoker(self, installer): - from letsencrypt.revoker import Revoker - return Revoker(installer, self.mock_config) - - def test_no_installer_get_installed_locations(self): - # pylint: disable=protected-access - revoker = self._get_revoker(None) - self.assertEqual(revoker._get_installed_locations(), {}) - - def test_get_installed_locations(self): - # pylint: disable=protected-access - mock_installer = mock.MagicMock() - mock_installer.get_all_certs_keys.return_value = self.certs_keys - - revoker = self._get_revoker(mock_installer) - sha_vh = revoker._get_installed_locations() - - self.assertEqual(len(sha_vh), 2) - for i, cert in enumerate(self.certs): - self.assertTrue(cert.get_fingerprint() in sha_vh) - self.assertEqual( - sha_vh[cert.get_fingerprint()], self.installs[i]) - - @mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_certificate") - def test_get_installed_load_failure(self, mock_load_certificate): - mock_installer = mock.MagicMock() - mock_installer.get_all_certs_keys.return_value = self.certs_keys - - mock_load_certificate.side_effect = OpenSSL.crypto.Error - - revoker = self._get_revoker(mock_installer) - - # pylint: disable=protected-access - self.assertEqual(revoker._get_installed_locations(), {}) - - def test_get_installed_load_failure_open(self): - tmp = tempfile.mkdtemp() - mock_installer = mock.MagicMock() - mock_installer.get_all_certs_keys.return_value = [( - os.path.join(tmp, 'missing'), None, None)] - revoker = self._get_revoker(mock_installer) - # pylint: disable=protected-access - self.assertEqual(revoker._get_installed_locations(), {}) - os.rmdir(tmp) - - -class RevokerClassMethodsTest(RevokerBase): - def setUp(self): - super(RevokerClassMethodsTest, self).setUp() - self.mock_config = mock.MagicMock(cert_key_backup=self.backup_dir) - - def tearDown(self): - shutil.rmtree(self.backup_dir) - - def _call(self, cert_path, key_path): - from letsencrypt.revoker import Revoker - Revoker.store_cert_key(cert_path, key_path, self.mock_config) - - def test_store_two(self): - from letsencrypt.revoker import Revoker - self._call(self.paths[0], self.key_path) - self._call(self.paths[1], self.key_path) - - self.assertTrue(os.path.isfile(self.list_path)) - rows = self._get_rows() - - for i, row in enumerate(rows): - # pylint: disable=protected-access - self.assertTrue(os.path.isfile( - Revoker._get_backup(self.backup_dir, i, self.paths[i]))) - self.assertTrue(os.path.isfile( - Revoker._get_backup(self.backup_dir, i, self.key_path))) - self.assertEqual([str(i), self.paths[i], self.key_path], row) - - self.assertEqual(len(rows), 2) - - def test_store_one_mixed(self): - from letsencrypt.revoker import Revoker - self._write_rows( - [["5", "blank", "blank"], ["18", "dc", "dc"], ["21", "b", "b"]]) - self._call(self.paths[0], self.key_path) - - self.assertEqual( - self._get_rows()[3], ["22", self.paths[0], self.key_path]) - - # pylint: disable=protected-access - self.assertTrue(os.path.isfile( - Revoker._get_backup(self.backup_dir, 22, self.paths[0]))) - self.assertTrue(os.path.isfile( - Revoker._get_backup(self.backup_dir, 22, self.key_path))) - - -class CertTest(unittest.TestCase): - def setUp(self): - self.paths, self.certs, self.key_path = create_revoker_certs() - - def test_failed_load(self): - from letsencrypt.revoker import Cert - self.assertRaises(errors.RevokerError, Cert, self.key_path) - - def test_failed_load_open(self): - tmp = tempfile.mkdtemp() - from letsencrypt.revoker import Cert - self.assertRaises( - errors.RevokerError, Cert, os.path.join(tmp, 'missing')) - os.rmdir(tmp) - - def test_no_row(self): - self.assertEqual(self.certs[0].get_row(), None) - - def test_meta_moved_files(self): - from letsencrypt.revoker import Cert - fake_path = "/not/a/real/path/r72d3t6" - self.certs[0].add_meta( - 0, fake_path, fake_path, self.paths[0], self.key_path) - - self.assertEqual(self.certs[0].orig.status, Cert.DELETED_MSG) - self.assertEqual(self.certs[0].orig_key.status, Cert.DELETED_MSG) - - def test_meta_changed_files(self): - from letsencrypt.revoker import Cert - self.certs[0].add_meta( - 0, self.paths[1], self.paths[1], self.paths[0], self.key_path) - - self.assertEqual(self.certs[0].orig.status, Cert.CHANGED_MSG) - self.assertEqual(self.certs[0].orig_key.status, Cert.CHANGED_MSG) - - def test_meta_no_status(self): - self.certs[0].add_meta( - 0, self.paths[0], self.key_path, self.paths[0], self.key_path) - - self.assertEqual(self.certs[0].orig.status, "") - self.assertEqual(self.certs[0].orig_key.status, "") - - def test_print_meta(self): - """Just make sure there aren't any major errors.""" - self.certs[0].add_meta( - 0, self.paths[0], self.key_path, self.paths[0], self.key_path) - # Changed path and deleted file - self.certs[1].add_meta( - 1, self.paths[0], "/not/a/path", self.paths[1], self.key_path) - self.assertTrue(self.certs[0].pretty_print()) - self.assertTrue(self.certs[1].pretty_print()) - - def test_print_no_meta(self): - self.assertTrue(self.certs[0].pretty_print()) - self.assertTrue(self.certs[1].pretty_print()) - - -def create_revoker_certs(): - """Create a few revoker.Cert objects.""" - cert0_path = test_util.vector_path("cert.pem") - cert1_path = test_util.vector_path("cert-san.pem") - key_path = test_util.vector_path("rsa512_key.pem") - - from letsencrypt.revoker import Cert - cert0 = Cert(cert0_path) - cert1 = Cert(cert1_path) - - return [cert0_path, cert1_path], [cert0, cert1], key_path - - -if __name__ == "__main__": - unittest.main() # pragma: no cover From c1a959de4532b3ca5ae1787338b45b3bf85dc6af Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 25 Sep 2015 22:44:33 -0700 Subject: [PATCH 027/145] Remove Revocation display --- letsencrypt/display/revocation.py | 77 ---------------- letsencrypt/tests/display/revocation_test.py | 97 -------------------- 2 files changed, 174 deletions(-) delete mode 100644 letsencrypt/display/revocation.py delete mode 100644 letsencrypt/tests/display/revocation_test.py diff --git a/letsencrypt/display/revocation.py b/letsencrypt/display/revocation.py deleted file mode 100644 index 02a253676..000000000 --- a/letsencrypt/display/revocation.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Revocation UI class.""" -import os - -import zope.component - -from letsencrypt import interfaces -from letsencrypt.display import util as display_util - -# Define a helper function to avoid verbose code -util = zope.component.getUtility # pylint: disable=invalid-name - - -def display_certs(certs): - """Display the certificates in a menu for revocation. - - :param list certs: each is a :class:`letsencrypt.revoker.Cert` - - :returns: tuple of the form (code, selection) where - code is a display exit code - selection is the user's int selection - :rtype: tuple - - """ - list_choices = [ - "%s | %s | %s" % ( - str(cert.get_cn().ljust(display_util.WIDTH - 39)), - cert.get_not_before().strftime("%m-%d-%y"), - "Installed" if cert.installed and cert.installed != ["Unknown"] - else "") for cert in certs - ] - - code, tag = util(interfaces.IDisplay).menu( - "Which certificates would you like to revoke?", - list_choices, help_label="More Info", ok_label="Revoke", - cancel_label="Exit") - - return code, tag - - -def confirm_revocation(cert): - """Confirm revocation screen. - - :param cert: certificate object - :type cert: :class: - - :returns: True if user would like to revoke, False otherwise - :rtype: bool - - """ - return util(interfaces.IDisplay).yesno( - "Are you sure you would like to revoke the following " - "certificate:{0}{cert}This action cannot be reversed!".format( - os.linesep, cert=cert.pretty_print())) - - -def more_info_cert(cert): - """Displays more info about the cert. - - :param dict cert: cert dict used throughout revoker.py - - """ - util(interfaces.IDisplay).notification( - "Certificate Information:{0}{1}".format( - os.linesep, cert.pretty_print()), - height=display_util.HEIGHT) - - -def success_revocation(cert): - """Display a success message. - - :param cert: cert that was revoked - :type cert: :class:`letsencrypt.revoker.Cert` - - """ - util(interfaces.IDisplay).notification( - "You have successfully revoked the certificate for " - "%s" % cert.get_cn()) diff --git a/letsencrypt/tests/display/revocation_test.py b/letsencrypt/tests/display/revocation_test.py deleted file mode 100644 index 6e9763006..000000000 --- a/letsencrypt/tests/display/revocation_test.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Test :mod:`letsencrypt.display.revocation`.""" -import sys -import unittest - -import mock -import zope.component - -from letsencrypt.display import util as display_util - -from letsencrypt.tests import test_util - - -class DisplayCertsTest(unittest.TestCase): - def setUp(self): - from letsencrypt.revoker import Cert - self.cert0 = Cert(test_util.vector_path("cert.pem")) - self.cert1 = Cert(test_util.vector_path("cert-san.pem")) - - self.certs = [self.cert0, self.cert1] - - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) - - @classmethod - def _call(cls, certs): - from letsencrypt.display.revocation import display_certs - return display_certs(certs) - - @mock.patch("letsencrypt.display.revocation.util") - def test_revocation(self, mock_util): - mock_util().menu.return_value = (display_util.OK, 0) - - code, choice = self._call(self.certs) - - self.assertEqual(display_util.OK, code) - self.assertEqual(self.certs[choice], self.cert0) - - @mock.patch("letsencrypt.display.revocation.util") - def test_cancel(self, mock_util): - mock_util().menu.return_value = (display_util.CANCEL, -1) - - code, _ = self._call(self.certs) - self.assertEqual(display_util.CANCEL, code) - - -class MoreInfoCertTest(unittest.TestCase): - # pylint: disable=too-few-public-methods - @classmethod - def _call(cls, cert): - from letsencrypt.display.revocation import more_info_cert - more_info_cert(cert) - - @mock.patch("letsencrypt.display.revocation.util") - def test_more_info(self, mock_util): - self._call(mock.MagicMock()) - - self.assertEqual(mock_util().notification.call_count, 1) - - -class SuccessRevocationTest(unittest.TestCase): - def setUp(self): - from letsencrypt.revoker import Cert - self.cert = Cert(test_util.vector_path("cert.pem")) - - @classmethod - def _call(cls, cert): - from letsencrypt.display.revocation import success_revocation - success_revocation(cert) - - # Pretty trivial test... something is displayed... - @mock.patch("letsencrypt.display.revocation.util") - def test_success_revocation(self, mock_util): - self._call(self.cert) - - self.assertEqual(mock_util().notification.call_count, 1) - - -class ConfirmRevocationTest(unittest.TestCase): - def setUp(self): - from letsencrypt.revoker import Cert - self.cert = Cert(test_util.vector_path("cert.pem")) - - @classmethod - def _call(cls, cert): - from letsencrypt.display.revocation import confirm_revocation - return confirm_revocation(cert) - - @mock.patch("letsencrypt.display.revocation.util") - def test_confirm_revocation(self, mock_util): - mock_util().yesno.return_value = True - self.assertTrue(self._call(self.cert)) - - mock_util().yesno.return_value = False - self.assertFalse(self._call(self.cert)) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover From f02653801df539df45518eb1887af876da984027 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 25 Sep 2015 22:54:15 -0700 Subject: [PATCH 028/145] Remove revocation from client --- letsencrypt/client.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 60eaea5a1..0eba8349d 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -21,7 +21,6 @@ from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt import reverter -from letsencrypt import revoker from letsencrypt import storage from letsencrypt.display import ops as display_ops @@ -485,27 +484,6 @@ def rollback(default_installer, checkpoints, config, plugins): installer.restart() -def revoke(default_installer, config, plugins, no_confirm, cert, authkey): - """Revoke certificates. - - :param config: Configuration. - :type config: :class:`letsencrypt.interfaces.IConfig` - - """ - installer = display_ops.pick_installer( - config, default_installer, plugins, question="Which installer " - "should be used for certificate revocation?") - - revoc = revoker.Revoker(installer, config, no_confirm) - # Cert is most selective, so it is chosen first. - if cert is not None: - revoc.revoke_from_cert(cert[0]) - elif authkey is not None: - revoc.revoke_from_key(le_util.Key(authkey[0], authkey[1])) - else: - revoc.revoke_from_menu() - - def view_config_changes(config): """View checkpoints and associated configuration changes. From 514fc49e696d5d1e546184a1b8afeac87933c5a2 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 25 Sep 2015 22:57:39 -0700 Subject: [PATCH 029/145] lower coverage due to removing revoker :( --- tox.cover.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.cover.sh b/tox.cover.sh index edfd9b81a..aa5e3ed88 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -16,7 +16,7 @@ fi cover () { if [ "$1" = "letsencrypt" ]; then - min=97 + min=96 elif [ "$1" = "acme" ]; then min=100 elif [ "$1" = "letsencrypt_apache" ]; then From 98d49ae8bf7b686601efda423fd5875249451671 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Sat, 26 Sep 2015 01:34:09 -0700 Subject: [PATCH 030/145] Remove excessive error handling --- letsencrypt/cli.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index cd34708b9..11ee65734 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -323,10 +323,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo # TODO: Handle errors from _init_le_client? le_client = _init_le_client(args, config, authenticator, installer) - try: - lineage = _auth_from_domains(le_client, config, domains, plugins) - except errors.Error as err: - return str(err) + lineage = _auth_from_domains(le_client, config, domains, plugins) # TODO: We also need to pass the fullchain (for Nginx) le_client.deploy_certificate( @@ -369,10 +366,7 @@ def auth(args, config, plugins): certr, chain, args.cert_path, args.chain_path) else: domains = _find_domains(args, installer) - try: - _auth_from_domains(le_client, config, domains, plugins) - except errors.Error as err: - return str(err) + _auth_from_domains(le_client, config, domains, plugins) def install(args, config, plugins): From 81f0a973a3452e1581c186c15fc5db6ffb218607 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 09:07:08 +0000 Subject: [PATCH 031/145] ManualAuthenticator -> Authenticator --- letsencrypt/plugins/manual.py | 4 ++-- letsencrypt/plugins/manual_test.py | 10 +++++----- setup.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 43d0ac055..2014c8c0e 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -23,7 +23,7 @@ from letsencrypt.plugins import common logger = logging.getLogger(__name__) -class ManualAuthenticator(common.Plugin): +class Authenticator(common.Plugin): """Manual Authenticator. .. todo:: Support for `~.challenges.DVSNI`. @@ -87,7 +87,7 @@ s.serve_forever()" """ """ def __init__(self, *args, **kwargs): - super(ManualAuthenticator, self).__init__(*args, **kwargs) + super(Authenticator, self).__init__(*args, **kwargs) self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls else self.HTTPS_TEMPLATE) self._root = (tempfile.mkdtemp() if self.conf("test-mode") diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index 6b9359db1..cfe47b833 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -17,22 +17,22 @@ from letsencrypt.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) -class ManualAuthenticatorTest(unittest.TestCase): - """Tests for letsencrypt.plugins.manual.ManualAuthenticator.""" +class AuthenticatorTest(unittest.TestCase): + """Tests for letsencrypt.plugins.manual.Authenticator.""" def setUp(self): - from letsencrypt.plugins.manual import ManualAuthenticator + from letsencrypt.plugins.manual import Authenticator self.config = mock.MagicMock( no_simple_http_tls=True, simple_http_port=4430, manual_test_mode=False) - self.auth = ManualAuthenticator(config=self.config, name="manual") + self.auth = Authenticator(config=self.config, name="manual") self.achalls = [achallenges.SimpleHTTP( challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( no_simple_http_tls=True, simple_http_port=4430, manual_test_mode=True) - self.auth_test_mode = ManualAuthenticator( + self.auth_test_mode = Authenticator( config=config_test_mode, name="manual") def test_more_info(self): diff --git a/setup.py b/setup.py index 6e1640e3e..ef7132edd 100644 --- a/setup.py +++ b/setup.py @@ -116,7 +116,7 @@ setup( 'letsencrypt-renewer = letsencrypt.renewer:main', ], 'letsencrypt.plugins': [ - 'manual = letsencrypt.plugins.manual:ManualAuthenticator', + 'manual = letsencrypt.plugins.manual:Authenticator', # TODO: null should probably not be presented to the user 'null = letsencrypt.plugins.null:Installer', 'standalone = letsencrypt.plugins.standalone.authenticator' From 08c0c4aebaa4dac9f8016f0399efe3c98472e532 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 24 Sep 2015 19:28:21 +0000 Subject: [PATCH 032/145] Explicit dependency on setuptools (pkg_resources). --- acme/setup.py | 1 + letsencrypt-apache/setup.py | 1 + letsencrypt-nginx/setup.py | 1 + letshelp-letsencrypt/setup.py | 4 +++- setup.py | 1 + 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/acme/setup.py b/acme/setup.py index 4cf215b40..60f97844b 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -16,6 +16,7 @@ install_requires = [ 'pyrfc3339', 'pytz', 'requests', + 'setuptools', # pkg_resources 'six', 'werkzeug', ] diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 5ecb071c7..57d2f6b47 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -7,6 +7,7 @@ install_requires = [ 'letsencrypt', 'mock<1.1.0', # py26 'python-augeas', + 'setuptools', # pkg_resources 'zope.component', 'zope.interface', ] diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 30dfa584f..b4ef69505 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -8,6 +8,7 @@ install_requires = [ 'mock<1.1.0', # py26 'PyOpenSSL', 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? + 'setuptools', # pkg_resources 'zope.interface', ] diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index 6b89a6d09..5e7542411 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -4,7 +4,9 @@ from setuptools import setup from setuptools import find_packages -install_requires = [] +install_requires = [ + 'setuptools', # pkg_resources +] if sys.version_info < (2, 7): install_requires.append("mock<1.1.0") else: diff --git a/setup.py b/setup.py index 6e1640e3e..8f743d4da 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ install_requires = [ 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 'pytz', 'requests', + 'setuptools', 'zope.component', 'zope.interface', ] From d337865f484ddc9aa3dda8628d845685e2a20c5d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 10:54:24 +0000 Subject: [PATCH 033/145] add missing pkg_resources comment --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8f743d4da..b43365a98 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ install_requires = [ 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 'pytz', 'requests', - 'setuptools', + 'setuptools', # pkg_resources 'zope.component', 'zope.interface', ] From 5128a0345ff613ebe151ee749275854741a0dc09 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 11:07:54 +0000 Subject: [PATCH 034/145] agree-tos in dev-cli.ini --- examples/dev-cli.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/dev-cli.ini b/examples/dev-cli.ini index 761bc58c9..085d4bfcc 100644 --- a/examples/dev-cli.ini +++ b/examples/dev-cli.ini @@ -9,6 +9,7 @@ domains = example.com text = True agree-eula = True +agree-tos = True debug = True # Unfortunately, it's not possible to specify "verbose" multiple times # (correspondingly to -vvvvvv) From 2015811a6c84682466005566afd795ea4c03f10f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Sat, 26 Sep 2015 12:18:32 -0700 Subject: [PATCH 035/145] Incorporated Kuba's feedback --- letsencrypt/colored_logging.py | 15 ++++----------- letsencrypt/le_util.py | 7 ++++++- letsencrypt/reporter.py | 3 +-- letsencrypt/tests/colored_logging_test.py | 11 ++++++----- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/letsencrypt/colored_logging.py b/letsencrypt/colored_logging.py index f5750870e..170da0b38 100644 --- a/letsencrypt/colored_logging.py +++ b/letsencrypt/colored_logging.py @@ -15,13 +15,12 @@ class StreamHandler(logging.StreamHandler): :ivar bool red_level: The level at which to output """ - _RED = '\033[31m' def __init__(self, stream=None): super(StreamHandler, self).__init__(stream) self.colored = (sys.stderr.isatty() if stream is None else stream.isatty()) - self.set_red_level(logging.WARNING) + self.red_level = logging.WARNING def format(self, record): """Formats the string representation of record. @@ -34,14 +33,8 @@ class StreamHandler(logging.StreamHandler): """ output = super(StreamHandler, self).format(record) if self.colored and record.levelno >= self.red_level: - return ''.join((self._RED, output, le_util.ANSI_SGR_RESET)) + return ''.join((le_util.ANSI_SGR_RED, + output, + le_util.ANSI_SGR_RESET)) else: return output - - def set_red_level(self, red_level): - """Sets the level necessary to display output in red. - - :param int red_level: Minimum log level for displaying red text - - """ - self.red_level = red_level diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 74e03d8a1..5626902ef 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -18,7 +18,12 @@ Key = collections.namedtuple("Key", "file pem") CSR = collections.namedtuple("CSR", "file data form") -# ANSI escape code for resetting output format +# ANSI SGR escape codes +# Formats text as bold or with increased intensity +ANSI_SGR_BOLD = '\033[1m' +# Colors text red +ANSI_SGR_RED = "\033[31m" +# Resets output format ANSI_SGR_RESET = "\033[0m" diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 86413053e..482305838 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -31,7 +31,6 @@ class Reporter(object): LOW_PRIORITY = 2 """Low priority constant. See `add_message`.""" - _BOLD = '\033[1m' _msg_type = collections.namedtuple('ReporterMsg', 'priority text on_crash') def __init__(self): @@ -76,7 +75,7 @@ class Reporter(object): no_exception = sys.exc_info()[0] is None bold_on = sys.stdout.isatty() if bold_on: - print self._BOLD + print le_util.ANSI_SGR_BOLD print 'IMPORTANT NOTES:' first_wrapper = textwrap.TextWrapper( initial_indent=' - ', subsequent_indent=(' ' * 3)) diff --git a/letsencrypt/tests/colored_logging_test.py b/letsencrypt/tests/colored_logging_test.py index fc97b2a49..5b49ec820 100644 --- a/letsencrypt/tests/colored_logging_test.py +++ b/letsencrypt/tests/colored_logging_test.py @@ -7,6 +7,7 @@ from letsencrypt import le_util class StreamHandlerTest(unittest.TestCase): + """Tests for letsencrypt.colored_logging.""" def setUp(self): from letsencrypt import colored_logging @@ -26,13 +27,13 @@ class StreamHandlerTest(unittest.TestCase): def test_format_and_red_level(self): msg = 'I did another thing' - self.handler.set_red_level(logging.DEBUG) + self.handler.red_level = logging.DEBUG self.logger.debug(msg) - # pylint: disable=protected-access - expected = '{0}{1}{2}\n'.format(self.handler._RED, msg, - le_util.ANSI_SGR_RESET) - self.assertEqual(self.stream.getvalue(), expected) + self.assertEqual(self.stream.getvalue(), + '{0}{1}{2}\n'.format(le_util.ANSI_SGR_RED, + msg, + le_util.ANSI_SGR_RESET)) if __name__ == "__main__": From 655c3c2a0eaa833577efdaa33a7c440fad7343b3 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Sat, 26 Sep 2015 15:44:57 -0700 Subject: [PATCH 036/145] Address comments --- letsencrypt/cli.py | 10 +++++----- letsencrypt/client.py | 16 +++++----------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 11ee65734..bd73c93d7 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -272,8 +272,10 @@ def _auth_from_domains(le_client, config, domains, plugins): lineage = _treat_as_renewal(config, domains) if lineage is not None: + # TODO: schoen wishes to reuse key - discussion + # https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574 new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) - # TODO: Check whether it worked! + # TODO: Check whether it worked! <- or make sure errors are thrown (jdk) lineage.save_successor( lineage.latest_common_version(), OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, new_certr.body), @@ -282,11 +284,10 @@ def _auth_from_domains(le_client, config, domains, plugins): lineage.update_all_links_to(lineage.latest_common_version()) # TODO: Check return value of save_successor # TODO: Also update lineage renewal config with any relevant - # configuration values from this attempt? - YES + # configuration values from this attempt? <- Absolutely (jdkasten) else: # TREAT AS NEW REQUEST - lineage = le_client.obtain_and_enroll_certificate( - domains, le_client.dv_auth, le_client.installer, plugins) + lineage = le_client.obtain_and_enroll_certificate(domains, plugins) if not lineage: raise errors.Error("Certificate could not be obtained") @@ -338,7 +339,6 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo def auth(args, config, plugins): """Authenticate & obtain cert, but do not install it.""" - # XXX: Update for renewer / RenewableCert if args.domains is not None and args.csr is not None: # TODO: --csr could have a priority, when --domains is diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 84ce9b7b2..39dd6ddfe 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -213,8 +213,7 @@ class Client(object): return self._obtain_certificate(domains, csr) + (key, csr) - def obtain_and_enroll_certificate( - self, domains, authenticator, installer, plugins): + def obtain_and_enroll_certificate(self, domains, plugins): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified @@ -222,12 +221,6 @@ class Client(object): containing it. :param list domains: Domains to request. - :param authenticator: The authenticator to use. - :type authenticator: :class:`letsencrypt.interfaces.IAuthenticator` - - :param installer: The installer to use. - :type installer: :class:`letsencrypt.interfaces.IInstaller` - :param plugins: A PluginsFactory object. :returns: A new :class:`letsencrypt.storage.RenewableCert` instance @@ -239,9 +232,10 @@ class Client(object): # TODO: remove this dirty hack self.config.namespace.authenticator = plugins.find_init( - authenticator).name - if installer is not None: - self.config.namespace.installer = plugins.find_init(installer).name + self.dv_auth).name + if self.installer is not None: + self.config.namespace.installer = plugins.find_init( + self.installer).name # XXX: We clearly need a more general and correct way of getting # options into the configobj for the RenewableCert instance. From 8dc345a3a0909612c88836c6f82b9290495c801c Mon Sep 17 00:00:00 2001 From: James Kasten Date: Sat, 26 Sep 2015 16:04:44 -0700 Subject: [PATCH 037/145] address naming conventions --- letsencrypt/cli.py | 7 ++++--- letsencrypt/tests/cli_test.py | 2 +- tests/integration/_common.sh | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index bd73c93d7..0804142f6 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -241,7 +241,7 @@ def _treat_as_renewal(config, domains): # We aren't in a duplicative-names situation at all, so we don't # have to tell or ask the user anything about this. pass - elif config.no_confirm or zope.component.getUtility( + elif config.renew_by_default or zope.component.getUtility( interfaces.IDisplay).yesno(question, "Replace", "Cancel"): renewal = True else: @@ -654,8 +654,9 @@ def create_parser(plugins, args): version="%(prog)s {0}".format(letsencrypt.__version__), help="show program's version number and exit") helpful.add( - "automation", "--no-confirm", dest="no_confirm", action="store_true", - help="Turn off confirmation screens, used for renewal screens") + "automation", "--renew-by-default", action="store_true", + help="Select renewal by default when domains are a superset of a " + "a previously attained cert") helpful.add( "automation", "--agree-eula", dest="eula", action="store_true", help="Agree to the Let's Encrypt Developer Preview EULA") diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 97725a4c7..2e9f3330c 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -177,7 +177,7 @@ class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest): shutil.rmtree(self.tempdir) @mock.patch("letsencrypt.le_util.make_or_verify_dir") - def test_find_duplicative_names(self, unused): # pylint: disable=unused-argument + def test_find_duplicative_names(self, unused_makedir): from letsencrypt.cli import _find_duplicative_certs test_cert = test_util.load_vector("cert-san.pem") with open(self.test_rc.cert, "w") as f: diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 7897ff1b7..fd60b9258 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -23,7 +23,7 @@ letsencrypt_test () { --agree-eula \ --agree-tos \ --email "" \ - --no-confirm \ + --renew-by-default \ --debug \ -vvvvvvv \ "$@" From 6f1b1570b13d9e41dceaca909ebf417469609ee7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 26 Sep 2015 17:48:45 -0700 Subject: [PATCH 038/145] Revert "ManualAuthenticator -> Authenticator" This reverts commit 81f0a973a3452e1581c186c15fc5db6ffb218607. This was breaking the client. Not sure if/how it passed any tests? --- letsencrypt/plugins/manual.py | 4 ++-- letsencrypt/plugins/manual_test.py | 10 +++++----- setup.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 2014c8c0e..43d0ac055 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -23,7 +23,7 @@ from letsencrypt.plugins import common logger = logging.getLogger(__name__) -class Authenticator(common.Plugin): +class ManualAuthenticator(common.Plugin): """Manual Authenticator. .. todo:: Support for `~.challenges.DVSNI`. @@ -87,7 +87,7 @@ s.serve_forever()" """ """ def __init__(self, *args, **kwargs): - super(Authenticator, self).__init__(*args, **kwargs) + super(ManualAuthenticator, self).__init__(*args, **kwargs) self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls else self.HTTPS_TEMPLATE) self._root = (tempfile.mkdtemp() if self.conf("test-mode") diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index cfe47b833..6b9359db1 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -17,22 +17,22 @@ from letsencrypt.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) -class AuthenticatorTest(unittest.TestCase): - """Tests for letsencrypt.plugins.manual.Authenticator.""" +class ManualAuthenticatorTest(unittest.TestCase): + """Tests for letsencrypt.plugins.manual.ManualAuthenticator.""" def setUp(self): - from letsencrypt.plugins.manual import Authenticator + from letsencrypt.plugins.manual import ManualAuthenticator self.config = mock.MagicMock( no_simple_http_tls=True, simple_http_port=4430, manual_test_mode=False) - self.auth = Authenticator(config=self.config, name="manual") + self.auth = ManualAuthenticator(config=self.config, name="manual") self.achalls = [achallenges.SimpleHTTP( challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( no_simple_http_tls=True, simple_http_port=4430, manual_test_mode=True) - self.auth_test_mode = Authenticator( + self.auth_test_mode = ManualAuthenticator( config=config_test_mode, name="manual") def test_more_info(self): diff --git a/setup.py b/setup.py index c568d2872..b43365a98 100644 --- a/setup.py +++ b/setup.py @@ -117,7 +117,7 @@ setup( 'letsencrypt-renewer = letsencrypt.renewer:main', ], 'letsencrypt.plugins': [ - 'manual = letsencrypt.plugins.manual:Authenticator', + 'manual = letsencrypt.plugins.manual:ManualAuthenticator', # TODO: null should probably not be presented to the user 'null = letsencrypt.plugins.null:Installer', 'standalone = letsencrypt.plugins.standalone.authenticator' From 63e1c652e18f98850f529173494cfbbd0a2905df Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 26 Sep 2015 18:05:17 -0700 Subject: [PATCH 039/145] Undo damage from PEP8ification --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 7cb4a0458..81f8a8414 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -484,7 +484,7 @@ class HelpfulArgumentParser(object): help2 = self.prescan_for_flag("--help", self.help_topics) assert max(True, "a") == "a", "Gravity changed direction" help_arg = max(help1, help2) - if help_arg: + if help_arg == True: # just --help with no topic; avoid argparse altogether print USAGE sys.exit(0) From 31fef196c0ed57a5cc6b1c4d409ff8097afbc716 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 27 Sep 2015 01:15:35 +0000 Subject: [PATCH 040/145] --help is effectively a verb for CLI purposes... --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 7cb4a0458..711ac0048 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -679,7 +679,7 @@ def create_parser(plugins, args): # For now unfortunately this constant just needs to match the code below; # there isn't an elegant way to autogenerate it in time. VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", - "plugins"] + "plugins", "--help"] def _create_subparsers(helpful): From 405bc99235754b661224a18753bdb8a8ec3ff60d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 26 Sep 2015 18:19:56 -0700 Subject: [PATCH 041/145] --help is effectively a verb for CLI purposes --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 81f8a8414..a5968ec9c 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -679,7 +679,7 @@ def create_parser(plugins, args): # For now unfortunately this constant just needs to match the code below; # there isn't an elegant way to autogenerate it in time. VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", - "plugins"] + "plugins", "--help"] def _create_subparsers(helpful): From e7cbdc4f9a0e021b385a8eb1869a21011d5e7840 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 26 Sep 2015 18:20:13 -0700 Subject: [PATCH 042/145] Revert reversion Revert "Revert "ManualAuthenticator -> Authenticator"" (commit required a pip reinstall but was not inherently broken) This reverts commit 6f1b1570b13d9e41dceaca909ebf417469609ee7. --- letsencrypt/plugins/manual.py | 4 ++-- letsencrypt/plugins/manual_test.py | 10 +++++----- setup.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 43d0ac055..2014c8c0e 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -23,7 +23,7 @@ from letsencrypt.plugins import common logger = logging.getLogger(__name__) -class ManualAuthenticator(common.Plugin): +class Authenticator(common.Plugin): """Manual Authenticator. .. todo:: Support for `~.challenges.DVSNI`. @@ -87,7 +87,7 @@ s.serve_forever()" """ """ def __init__(self, *args, **kwargs): - super(ManualAuthenticator, self).__init__(*args, **kwargs) + super(Authenticator, self).__init__(*args, **kwargs) self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls else self.HTTPS_TEMPLATE) self._root = (tempfile.mkdtemp() if self.conf("test-mode") diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index 6b9359db1..cfe47b833 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -17,22 +17,22 @@ from letsencrypt.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) -class ManualAuthenticatorTest(unittest.TestCase): - """Tests for letsencrypt.plugins.manual.ManualAuthenticator.""" +class AuthenticatorTest(unittest.TestCase): + """Tests for letsencrypt.plugins.manual.Authenticator.""" def setUp(self): - from letsencrypt.plugins.manual import ManualAuthenticator + from letsencrypt.plugins.manual import Authenticator self.config = mock.MagicMock( no_simple_http_tls=True, simple_http_port=4430, manual_test_mode=False) - self.auth = ManualAuthenticator(config=self.config, name="manual") + self.auth = Authenticator(config=self.config, name="manual") self.achalls = [achallenges.SimpleHTTP( challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( no_simple_http_tls=True, simple_http_port=4430, manual_test_mode=True) - self.auth_test_mode = ManualAuthenticator( + self.auth_test_mode = Authenticator( config=config_test_mode, name="manual") def test_more_info(self): diff --git a/setup.py b/setup.py index b43365a98..c568d2872 100644 --- a/setup.py +++ b/setup.py @@ -117,7 +117,7 @@ setup( 'letsencrypt-renewer = letsencrypt.renewer:main', ], 'letsencrypt.plugins': [ - 'manual = letsencrypt.plugins.manual:ManualAuthenticator', + 'manual = letsencrypt.plugins.manual:Authenticator', # TODO: null should probably not be presented to the user 'null = letsencrypt.plugins.null:Installer', 'standalone = letsencrypt.plugins.standalone.authenticator' From 001d37f9650d0c5f7521673f9fd075a7bd662b0c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 27 Sep 2015 02:41:55 +0000 Subject: [PATCH 043/145] "-h" is also a ver. --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index a5968ec9c..2c996cd3e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -679,7 +679,7 @@ def create_parser(plugins, args): # For now unfortunately this constant just needs to match the code below; # there isn't an elegant way to autogenerate it in time. VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", - "plugins", "--help"] + "plugins", "--help", "-h"] def _create_subparsers(helpful): From f3c2a096b54950e5368907b698c488a458180961 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 27 Sep 2015 02:48:44 +0000 Subject: [PATCH 044/145] Move the verb/subcommand to the end of the argparse line --- letsencrypt/cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 2c996cd3e..8afbf023f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -495,13 +495,14 @@ class HelpfulArgumentParser(object): def preprocess_args(self, args): """Work around some limitations in argparse. - Currently, add the default verb "run" as a default. + Currently: add the default verb "run" as a default, and ensure that the + subcommand / verb comes last. """ - - for token in args: + for i,token in enumerate(args): if token in VERBS: - return args - return ["run"] + args + reordered = args[:i] + args[i+1:] + [args[i]] + return reordered + return args + ["run"] def prescan_for_flag(self, flag, possible_arguments): """Checks cli input for flags. From 746016be6cfd17b8c5f19ab45eed7d2e1c11ac22 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 22:27:04 +0000 Subject: [PATCH 045/145] Sync version strings to 0.1.0.dev0, pin same repo deps. --- acme/setup.py | 3 +++ letsencrypt-apache/setup.py | 7 +++++-- letsencrypt-compatibility-test/setup.py | 9 ++++++--- letsencrypt-nginx/setup.py | 7 +++++-- letsencrypt/__init__.py | 2 +- letshelp-letsencrypt/setup.py | 7 +++++-- setup.py | 5 +++-- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 60f97844b..bec7feb25 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,6 +4,8 @@ from setuptools import setup from setuptools import find_packages +version = '0.1.0.dev0' + install_requires = [ # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) @@ -34,6 +36,7 @@ testing_extras = [ setup( name='acme', + version=version, packages=find_packages(), install_requires=install_requires, extras_require={ diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 57d2f6b47..5ac27f4fe 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -2,9 +2,11 @@ from setuptools import setup from setuptools import find_packages +version = '0.1.0.dev0' + install_requires = [ - 'acme', - 'letsencrypt', + 'acme=={0}'.format(version), + 'letsencrypt=={0}'.format(version), 'mock<1.1.0', # py26 'python-augeas', 'setuptools', # pkg_resources @@ -14,6 +16,7 @@ install_requires = [ setup( name='letsencrypt-apache', + version=version, packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index f02041e55..8d4bbda30 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -2,10 +2,12 @@ from setuptools import setup from setuptools import find_packages +version = '0.1.0.dev0' + install_requires = [ - 'letsencrypt', - 'letsencrypt-apache', - 'letsencrypt-nginx', + 'letsencrypt=={0}'.format(version), + 'letsencrypt-apache=={0}'.format(version), + 'letsencrypt-nginx=={0}'.format(version), 'docker-py', 'mock<1.1.0', # py26 'zope.interface', @@ -13,6 +15,7 @@ install_requires = [ setup( name='letsencrypt-compatibility-test', + version=version, packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index b4ef69505..0131f26cd 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -2,9 +2,11 @@ from setuptools import setup from setuptools import find_packages +version = '0.1.0.dev0' + install_requires = [ - 'acme', - 'letsencrypt', + 'acme=={0}'.format(version), + 'letsencrypt=={0}'.format(version), 'mock<1.1.0', # py26 'PyOpenSSL', 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? @@ -14,6 +16,7 @@ install_requires = [ setup( name='letsencrypt-nginx', + version=version, packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 560191bf1..e59dedeac 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,4 +1,4 @@ """Let's Encrypt client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = "0.1" +__version__ = "0.1.0.dev0" diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index 5e7542411..a228fcf09 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -4,8 +4,10 @@ from setuptools import setup from setuptools import find_packages +version = "0.1.0.dev0" + install_requires = [ - 'setuptools', # pkg_resources + "setuptools", # pkg_resources ] if sys.version_info < (2, 7): install_requires.append("mock<1.1.0") @@ -14,10 +16,11 @@ else: setup( name="letshelp-letsencrypt", + version=version, packages=find_packages(), install_requires=install_requires, entry_points={ - 'console_scripts': [ + "console_scripts": [ "letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main", ], }, diff --git a/setup.py b/setup.py index c568d2872..b753a8253 100644 --- a/setup.py +++ b/setup.py @@ -28,9 +28,10 @@ meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn))) readme = read_file(os.path.join(here, 'README.rst')) changes = read_file(os.path.join(here, 'CHANGES.rst')) +version = meta['version'] install_requires = [ - 'acme', + 'acme=={0}'.format(version), 'ConfigArgParse', 'configobj', 'cryptography>=0.7', # load_pem_x509_certificate @@ -75,7 +76,7 @@ testing_extras = [ setup( name='letsencrypt', - version=meta['version'], + version=version, description="Let's Encrypt", long_description=readme, # later: + '\n\n' + changes author="Let's Encrypt Project", From 20131de9fb63eeb7197c0067c4598219834e9bd6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 22:37:50 +0000 Subject: [PATCH 046/145] Add licences to all subpackages. --- MANIFEST.in | 1 + acme/LICENSE.txt | 190 +++++++++++++++++++++ acme/MANIFEST.in | 1 + acme/setup.py | 1 + letsencrypt-apache/LICENSE.txt | 190 +++++++++++++++++++++ letsencrypt-apache/MANIFEST.in | 1 + letsencrypt-apache/setup.py | 1 + letsencrypt-compatibility-test/LICENSE.txt | 190 +++++++++++++++++++++ letsencrypt-compatibility-test/MANIFEST.in | 1 + letsencrypt-compatibility-test/setup.py | 1 + letsencrypt-nginx/LICENSE.txt | 190 +++++++++++++++++++++ letsencrypt-nginx/MANIFEST.in | 1 + letsencrypt-nginx/setup.py | 1 + letshelp-letsencrypt/LICENSE.txt | 190 +++++++++++++++++++++ letshelp-letsencrypt/MANIFEST.in | 1 + letshelp-letsencrypt/setup.py | 1 + 16 files changed, 961 insertions(+) create mode 100644 acme/LICENSE.txt create mode 100644 letsencrypt-apache/LICENSE.txt create mode 100644 letsencrypt-compatibility-test/LICENSE.txt create mode 100644 letsencrypt-nginx/LICENSE.txt create mode 100644 letshelp-letsencrypt/LICENSE.txt diff --git a/MANIFEST.in b/MANIFEST.in index 530044212..80fd8777e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include requirements.txt include README.rst include CHANGES.rst include CONTRIBUTING.md +include LICENSE.txt include linter_plugin.py include letsencrypt/EULA recursive-include letsencrypt/tests/testdata * diff --git a/acme/LICENSE.txt b/acme/LICENSE.txt new file mode 100644 index 000000000..7c13afb9d --- /dev/null +++ b/acme/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Internet Security Research Group + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/acme/MANIFEST.in b/acme/MANIFEST.in index f3444f746..4a68ae096 100644 --- a/acme/MANIFEST.in +++ b/acme/MANIFEST.in @@ -1 +1,2 @@ +include LICENSE.txt recursive-include acme/testdata * diff --git a/acme/setup.py b/acme/setup.py index bec7feb25..80f8c2387 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -37,6 +37,7 @@ testing_extras = [ setup( name='acme', version=version, + license='Apache License 2.0', packages=find_packages(), install_requires=install_requires, extras_require={ diff --git a/letsencrypt-apache/LICENSE.txt b/letsencrypt-apache/LICENSE.txt new file mode 100644 index 000000000..7c13afb9d --- /dev/null +++ b/letsencrypt-apache/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Internet Security Research Group + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/letsencrypt-apache/MANIFEST.in b/letsencrypt-apache/MANIFEST.in index aac2bfb36..0a2a07efd 100644 --- a/letsencrypt-apache/MANIFEST.in +++ b/letsencrypt-apache/MANIFEST.in @@ -1,2 +1,3 @@ +include LICENSE.txt recursive-include letsencrypt_apache/tests/testdata * include letsencrypt_apache/options-ssl-apache.conf diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 5ac27f4fe..a3dd6989d 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -17,6 +17,7 @@ install_requires = [ setup( name='letsencrypt-apache', version=version, + license='Apache License 2.0', packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letsencrypt-compatibility-test/LICENSE.txt b/letsencrypt-compatibility-test/LICENSE.txt new file mode 100644 index 000000000..7c13afb9d --- /dev/null +++ b/letsencrypt-compatibility-test/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Internet Security Research Group + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/letsencrypt-compatibility-test/MANIFEST.in b/letsencrypt-compatibility-test/MANIFEST.in index a6aa14443..50a02378f 100644 --- a/letsencrypt-compatibility-test/MANIFEST.in +++ b/letsencrypt-compatibility-test/MANIFEST.in @@ -1 +1,2 @@ +include LICENSE.txt recursive-include letsencrypt_compatibility_test/testdata * diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index 8d4bbda30..e46a05a07 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -16,6 +16,7 @@ install_requires = [ setup( name='letsencrypt-compatibility-test', version=version, + license='Apache License 2.0', packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letsencrypt-nginx/LICENSE.txt b/letsencrypt-nginx/LICENSE.txt new file mode 100644 index 000000000..7c13afb9d --- /dev/null +++ b/letsencrypt-nginx/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Internet Security Research Group + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/letsencrypt-nginx/MANIFEST.in b/letsencrypt-nginx/MANIFEST.in index 94f85e40f..03457a674 100644 --- a/letsencrypt-nginx/MANIFEST.in +++ b/letsencrypt-nginx/MANIFEST.in @@ -1,2 +1,3 @@ +include LICENSE.txt recursive-include letsencrypt_nginx/tests/testdata * include letsencrypt_nginx/options-ssl-nginx.conf diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 0131f26cd..9d7d246b7 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -17,6 +17,7 @@ install_requires = [ setup( name='letsencrypt-nginx', version=version, + license='Apache License 2.0', packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letshelp-letsencrypt/LICENSE.txt b/letshelp-letsencrypt/LICENSE.txt new file mode 100644 index 000000000..7c13afb9d --- /dev/null +++ b/letshelp-letsencrypt/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Internet Security Research Group + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/letshelp-letsencrypt/MANIFEST.in b/letshelp-letsencrypt/MANIFEST.in index 61a3d3150..9173a6b57 100644 --- a/letshelp-letsencrypt/MANIFEST.in +++ b/letshelp-letsencrypt/MANIFEST.in @@ -1 +1,2 @@ +include LICENSE.txt recursive-include letshelp-letsencrypt/testdata * diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index a228fcf09..6582ba59c 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -17,6 +17,7 @@ else: setup( name="letshelp-letsencrypt", version=version, + license="Apache License 2.0", packages=find_packages(), install_requires=install_requires, entry_points={ From b5036e36ad928522e263db3b4398109167e30ddd Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 22:49:46 +0000 Subject: [PATCH 047/145] Unify setup.py: description/url/author/author_email. --- acme/setup.py | 5 +++++ letsencrypt-apache/setup.py | 5 +++++ letsencrypt-compatibility-test/setup.py | 5 +++++ letsencrypt-nginx/setup.py | 5 +++++ letshelp-letsencrypt/setup.py | 5 +++++ setup.py | 5 +++-- 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 80f8c2387..f3afe97c3 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -37,7 +37,12 @@ testing_extras = [ setup( name='acme', version=version, + description='ACME protocol implementation', + url='https://github.com/letsencrypt/letsencrypt', + author="Let's Encrypt Project", + author_email='client-dev@letsencrypt.org', license='Apache License 2.0', + packages=find_packages(), install_requires=install_requires, extras_require={ diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index a3dd6989d..035a10f4c 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -17,7 +17,12 @@ install_requires = [ setup( name='letsencrypt-apache', version=version, + description="Apache plugin for Let's Encrypt client", + url='https://github.com/letsencrypt/letsencrypt', + author="Let's Encrypt Project", + author_email='client-dev@letsencrypt.org', license='Apache License 2.0', + packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index e46a05a07..485752e64 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -16,7 +16,12 @@ install_requires = [ setup( name='letsencrypt-compatibility-test', version=version, + description="Compatibility tests for Let's Encrypt client", + url='https://github.com/letsencrypt/letsencrypt', + author="Let's Encrypt Project", + author_email='client-dev@letsencrypt.org', license='Apache License 2.0', + packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 9d7d246b7..0a6d9646e 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -17,7 +17,12 @@ install_requires = [ setup( name='letsencrypt-nginx', version=version, + description="Nginx plugin for Let's Encrypt client", + url='https://github.com/letsencrypt/letsencrypt', + author="Let's Encrypt Project", + author_email='client-dev@letsencrypt.org', license='Apache License 2.0', + packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index 6582ba59c..c2981132d 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -17,7 +17,12 @@ else: setup( name="letshelp-letsencrypt", version=version, + description="Let's help Let's Encrypt client", + url='https://github.com/letsencrypt/letsencrypt', + author="Let's Encrypt Project", + author_email='client-dev@letsencrypt.org', license="Apache License 2.0", + packages=find_packages(), install_requires=install_requires, entry_points={ diff --git a/setup.py b/setup.py index b753a8253..846c609e5 100644 --- a/setup.py +++ b/setup.py @@ -77,11 +77,12 @@ testing_extras = [ setup( name='letsencrypt', version=version, - description="Let's Encrypt", + description="Let's Encrypt client", long_description=readme, # later: + '\n\n' + changes + url='https://github.com/letsencrypt/letsencrypt', author="Let's Encrypt Project", + author_email='client-dev@letsencrypt.org', license='Apache License 2.0', - url='https://letsencrypt.org', classifiers=[ 'Environment :: Console', 'Environment :: Console :: Curses', From b6819ad05bb57d98fb78e5f1bd00841da7183d06 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 23:00:25 +0000 Subject: [PATCH 048/145] Add general classifiers to all setup.py scripts. --- acme/setup.py | 9 +++++++++ letsencrypt-apache/setup.py | 15 +++++++++++++++ letsencrypt-compatibility-test/setup.py | 9 +++++++++ letsencrypt-nginx/setup.py | 15 +++++++++++++++ letshelp-letsencrypt/setup.py | 14 ++++++++++++++ 5 files changed, 62 insertions(+) diff --git a/acme/setup.py b/acme/setup.py index f3afe97c3..4651ef5c2 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -42,6 +42,15 @@ setup( author="Let's Encrypt Project", author_email='client-dev@letsencrypt.org', license='Apache License 2.0', + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + ], packages=find_packages(), install_requires=install_requires, diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 035a10f4c..8d12b8ef2 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -22,6 +22,21 @@ setup( author="Let's Encrypt Project", author_email='client-dev@letsencrypt.org', license='Apache License 2.0', + classifiers=[ + 'Environment :: Plugins', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Networking', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], packages=find_packages(), install_requires=install_requires, diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index 485752e64..e69c67575 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -21,6 +21,15 @@ setup( author="Let's Encrypt Project", author_email='client-dev@letsencrypt.org', license='Apache License 2.0', + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + ], packages=find_packages(), install_requires=install_requires, diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 0a6d9646e..8798c9eb9 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -22,6 +22,21 @@ setup( author="Let's Encrypt Project", author_email='client-dev@letsencrypt.org', license='Apache License 2.0', + classifiers=[ + 'Environment :: Plugins', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Networking', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], packages=find_packages(), install_requires=install_requires, diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index c2981132d..f05ec0cd1 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -22,6 +22,20 @@ setup( author="Let's Encrypt Project", author_email='client-dev@letsencrypt.org', license="Apache License 2.0", + classifiers=[ + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Networking', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], packages=find_packages(), install_requires=install_requires, From 033ed589cc5ab5d08310779ce96ce28b88ca1ebb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 23:02:41 +0000 Subject: [PATCH 049/145] Development Status :: 3 - Alpha --- acme/setup.py | 1 + letsencrypt-apache/setup.py | 1 + letsencrypt-compatibility-test/setup.py | 1 + letsencrypt-nginx/setup.py | 1 + letshelp-letsencrypt/setup.py | 1 + setup.py | 1 + 6 files changed, 6 insertions(+) diff --git a/acme/setup.py b/acme/setup.py index 4651ef5c2..0bd03b050 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -43,6 +43,7 @@ setup( author_email='client-dev@letsencrypt.org', license='Apache License 2.0', classifiers=[ + 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 8d12b8ef2..4e3b441a0 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -23,6 +23,7 @@ setup( author_email='client-dev@letsencrypt.org', license='Apache License 2.0', classifiers=[ + 'Development Status :: 3 - Alpha', 'Environment :: Plugins', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index e69c67575..701b85f3a 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -22,6 +22,7 @@ setup( author_email='client-dev@letsencrypt.org', license='Apache License 2.0', classifiers=[ + 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 8798c9eb9..bf200b07b 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -23,6 +23,7 @@ setup( author_email='client-dev@letsencrypt.org', license='Apache License 2.0', classifiers=[ + 'Development Status :: 3 - Alpha', 'Environment :: Plugins', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index f05ec0cd1..e68813083 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -23,6 +23,7 @@ setup( author_email='client-dev@letsencrypt.org', license="Apache License 2.0", classifiers=[ + 'Development Status :: 3 - Alpha', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX :: Linux', diff --git a/setup.py b/setup.py index 846c609e5..3076d76b5 100644 --- a/setup.py +++ b/setup.py @@ -84,6 +84,7 @@ setup( author_email='client-dev@letsencrypt.org', license='Apache License 2.0', classifiers=[ + 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Environment :: Console :: Curses', 'Intended Audience :: System Administrators', From 9883f8965d55ee6992816cf9e409244a5b2303a4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 06:22:46 +0000 Subject: [PATCH 050/145] Add dummy README.rst files --- acme/MANIFEST.in | 1 + acme/README.rst | 1 + letsencrypt-apache/MANIFEST.in | 1 + letsencrypt-apache/README.rst | 1 + letsencrypt-compatibility-test/MANIFEST.in | 1 + letsencrypt-compatibility-test/README.rst | 1 + letsencrypt-nginx/MANIFEST.in | 1 + letsencrypt-nginx/README.rst | 1 + letshelp-letsencrypt/MANIFEST.in | 1 + letshelp-letsencrypt/README.rst | 1 + 10 files changed, 10 insertions(+) create mode 100644 acme/README.rst create mode 100644 letsencrypt-apache/README.rst create mode 100644 letsencrypt-compatibility-test/README.rst create mode 100644 letsencrypt-nginx/README.rst create mode 100644 letshelp-letsencrypt/README.rst diff --git a/acme/MANIFEST.in b/acme/MANIFEST.in index 4a68ae096..ec2b09e05 100644 --- a/acme/MANIFEST.in +++ b/acme/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE.txt +include README.rst recursive-include acme/testdata * diff --git a/acme/README.rst b/acme/README.rst new file mode 100644 index 000000000..e3ca8b738 --- /dev/null +++ b/acme/README.rst @@ -0,0 +1 @@ +ACME protocol implementation for Python diff --git a/letsencrypt-apache/MANIFEST.in b/letsencrypt-apache/MANIFEST.in index 0a2a07efd..ff99bf0d8 100644 --- a/letsencrypt-apache/MANIFEST.in +++ b/letsencrypt-apache/MANIFEST.in @@ -1,3 +1,4 @@ include LICENSE.txt +include README.rst recursive-include letsencrypt_apache/tests/testdata * include letsencrypt_apache/options-ssl-apache.conf diff --git a/letsencrypt-apache/README.rst b/letsencrypt-apache/README.rst new file mode 100644 index 000000000..3505fd594 --- /dev/null +++ b/letsencrypt-apache/README.rst @@ -0,0 +1 @@ +Apache plugin for Let's Encrypt client diff --git a/letsencrypt-compatibility-test/MANIFEST.in b/letsencrypt-compatibility-test/MANIFEST.in index 50a02378f..52bbb3c65 100644 --- a/letsencrypt-compatibility-test/MANIFEST.in +++ b/letsencrypt-compatibility-test/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE.txt +include README.rst recursive-include letsencrypt_compatibility_test/testdata * diff --git a/letsencrypt-compatibility-test/README.rst b/letsencrypt-compatibility-test/README.rst new file mode 100644 index 000000000..4afd999a8 --- /dev/null +++ b/letsencrypt-compatibility-test/README.rst @@ -0,0 +1 @@ +Compatibility tests for Let's Encrypt client diff --git a/letsencrypt-nginx/MANIFEST.in b/letsencrypt-nginx/MANIFEST.in index 03457a674..c4bd67735 100644 --- a/letsencrypt-nginx/MANIFEST.in +++ b/letsencrypt-nginx/MANIFEST.in @@ -1,3 +1,4 @@ include LICENSE.txt +include README.rst recursive-include letsencrypt_nginx/tests/testdata * include letsencrypt_nginx/options-ssl-nginx.conf diff --git a/letsencrypt-nginx/README.rst b/letsencrypt-nginx/README.rst new file mode 100644 index 000000000..ff6d50ce4 --- /dev/null +++ b/letsencrypt-nginx/README.rst @@ -0,0 +1 @@ +Nginx plugin for Let's Encrypt client diff --git a/letshelp-letsencrypt/MANIFEST.in b/letshelp-letsencrypt/MANIFEST.in index 9173a6b57..380b80fcb 100644 --- a/letshelp-letsencrypt/MANIFEST.in +++ b/letshelp-letsencrypt/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE.txt +include README.rst recursive-include letshelp-letsencrypt/testdata * diff --git a/letshelp-letsencrypt/README.rst b/letshelp-letsencrypt/README.rst new file mode 100644 index 000000000..159048d6d --- /dev/null +++ b/letshelp-letsencrypt/README.rst @@ -0,0 +1 @@ +Let's help Let's Encrypt client From 3d638caeb7daca282035b8b7b46c2761d5a1c94a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 06:30:30 +0000 Subject: [PATCH 051/145] Unify quotes in setup.py scripts --- letsencrypt/__init__.py | 2 +- letshelp-letsencrypt/setup.py | 16 ++++++++-------- setup.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index e59dedeac..1155a5b0c 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,4 +1,4 @@ """Let's Encrypt client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = "0.1.0.dev0" +__version__ = '0.1.0.dev0' diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index e68813083..9291b101d 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -4,24 +4,24 @@ from setuptools import setup from setuptools import find_packages -version = "0.1.0.dev0" +version = '0.1.0.dev0' install_requires = [ - "setuptools", # pkg_resources + 'setuptools', # pkg_resources ] if sys.version_info < (2, 7): - install_requires.append("mock<1.1.0") + install_requires.append('mock<1.1.0') else: - install_requires.append("mock") + install_requires.append('mock') setup( - name="letshelp-letsencrypt", + name='letshelp-letsencrypt', version=version, description="Let's help Let's Encrypt client", url='https://github.com/letsencrypt/letsencrypt', author="Let's Encrypt Project", author_email='client-dev@letsencrypt.org', - license="Apache License 2.0", + license='Apache License 2.0', classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: System Administrators', @@ -41,8 +41,8 @@ setup( packages=find_packages(), install_requires=install_requires, entry_points={ - "console_scripts": [ - "letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main", + 'console_scripts': [ + 'letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main', ], }, include_package_data=True, diff --git a/setup.py b/setup.py index 3076d76b5..b1b0213b6 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ here = os.path.abspath(os.path.dirname(__file__)) # read version number (and other metadata) from package init init_fn = os.path.join(here, 'letsencrypt', '__init__.py') -meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn))) +meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", read_file(init_fn))) readme = read_file(os.path.join(here, 'README.rst')) changes = read_file(os.path.join(here, 'CHANGES.rst')) From ddc04c755bf6a3e9da956ecf8782ee32b6464cc0 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 27 Sep 2015 07:56:38 +0000 Subject: [PATCH 052/145] work in progress --- letsencrypt/cli.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 6d3aa9d2c..53609009b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -730,53 +730,55 @@ def _create_subparsers(helpful): # the order of add_subparser() calls is important: it defines the # order in which subparser names will be displayed in --help add_subparser("run", run) + parser_auth = add_subparser("auth", auth) + helpful.add_group("auth", "Options for modifying how a cert is obtained") parser_install = add_subparser("install", install) + helpful.add_group("install", "Options for modifying how a cert is deployed") parser_revoke = add_subparser("revoke", revoke) + helpful.add_group("revoke", "Options for revocation of certs") parser_rollback = add_subparser("rollback", rollback) + helpful.add_group("rollback", "Options for reverting config changes") add_subparser("config_changes", config_changes) parser_plugins = add_subparser("plugins", plugins_cmd) + helpful.add_group("plugins", "Plugin options") - parser_auth.add_argument( - "--csr", type=read_file, help="Path to a Certificate Signing " - "Request (CSR) in DER format.") - parser_auth.add_argument( + helpful.add("auth", + "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER format.") + helpful.add("auth", "--cert-path", default=flag_default("auth_cert_path"), help="When using --csr this is where certificate is saved.") - parser_auth.add_argument( + helpful.add("auth", "--chain-path", default=flag_default("auth_chain_path"), help="When using --csr this is where certificate chain is saved.") - parser_install.add_argument( - "--cert-path", required=True, help="Path to a certificate that " - "is going to be installed.") - parser_install.add_argument( + helpful.add("install", + "--cert-path", required=True, help="Path to a certificate that is going to be installed.") + helpful.add("install", "--key-path", required=True, help="Accompanying private key") - parser_install.add_argument( + helpful.add("install", "--chain-path", help="Accompanying path to a certificate chain.") - parser_revoke.add_argument( - "--cert-path", type=read_file, help="Revoke a specific certificate.", - required=True) - parser_revoke.add_argument( + helpful.add("revoke", + "--cert-path", type=read_file, help="Revoke a specific certificate.", required=True) + helpful.add("revoke", "--key-path", type=read_file, - help="Revoke certificate using its accompanying key. Useful if " - "Account Key is lost.") + help="Revoke certificate using its accompanying key. Useful if Account Key is lost.") - parser_rollback.add_argument( + helpful.add("rollback", "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), help="Revert configuration N number of checkpoints.") - parser_plugins.add_argument( + helpful.add("plugins", "--init", action="store_true", help="Initialize plugins.") - parser_plugins.add_argument( + helpful.add("plugins", "--prepare", action="store_true", help="Initialize and prepare plugins.") - parser_plugins.add_argument( + helpful.add("plugins", "--authenticators", action="append_const", dest="ifaces", const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.") - parser_plugins.add_argument( + helpful.add("plugins", "--installers", action="append_const", dest="ifaces", const=interfaces.IInstaller, help="Limit to installer plugins only.") From 45a0cd2799a8e3f4b59164045ccf41bf54fec5f3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 08:10:39 +0000 Subject: [PATCH 053/145] Fix include_package_data. --- acme/setup.py | 1 + letsencrypt-apache/setup.py | 2 +- letsencrypt-compatibility-test/setup.py | 1 + letsencrypt-nginx/setup.py | 2 +- letshelp-letsencrypt/setup.py | 2 +- setup.py | 5 ++--- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 0bd03b050..2a3a123c5 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -54,6 +54,7 @@ setup( ], packages=find_packages(), + include_package_data=True, install_requires=install_requires, extras_require={ 'testing': testing_extras, diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 4e3b441a0..ee1457131 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -40,11 +40,11 @@ setup( ], packages=find_packages(), + include_package_data=True, install_requires=install_requires, entry_points={ 'letsencrypt.plugins': [ 'apache = letsencrypt_apache.configurator:ApacheConfigurator', ], }, - include_package_data=True, ) diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index 701b85f3a..745b49bb5 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -33,6 +33,7 @@ setup( ], packages=find_packages(), + include_package_data=True, install_requires=install_requires, entry_points={ 'console_scripts': [ diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index bf200b07b..4e770c8cb 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -40,11 +40,11 @@ setup( ], packages=find_packages(), + include_package_data=True, install_requires=install_requires, entry_points={ 'letsencrypt.plugins': [ 'nginx = letsencrypt_nginx.configurator:NginxConfigurator', ], }, - include_package_data=True, ) diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index 9291b101d..a83fc8843 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -39,11 +39,11 @@ setup( ], packages=find_packages(), + include_package_data=True, install_requires=install_requires, entry_points={ 'console_scripts': [ 'letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main', ], }, - include_package_data=True, ) diff --git a/setup.py b/setup.py index b1b0213b6..0fed79354 100644 --- a/setup.py +++ b/setup.py @@ -102,6 +102,8 @@ setup( ], packages=find_packages(exclude=['docs', 'examples', 'tests', 'venv']), + include_package_data=True, + install_requires=install_requires, extras_require={ 'dev': dev_extras, @@ -127,7 +129,4 @@ setup( ':StandaloneAuthenticator', ], }, - - zip_safe=False, # letsencrypt/tests/test_util.py is a symlink! - include_package_data=True, ) From d621df3320fe5d291c1da4884365f11e659b2127 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 10:50:50 +0000 Subject: [PATCH 054/145] Make zipsafe --- acme/acme/test_util.py | 2 - letsencrypt/tests/test_util.py | 68 +++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) mode change 120000 => 100644 letsencrypt/tests/test_util.py diff --git a/acme/acme/test_util.py b/acme/acme/test_util.py index c9c076d27..2b4c6e00c 100644 --- a/acme/acme/test_util.py +++ b/acme/acme/test_util.py @@ -1,5 +1,3 @@ -# Symlinked in letsencrypt/tests/test_util.py, causes duplicate-code -# warning that cannot be disabled locally. """Test utilities. .. warning:: This module is not part of the public API. diff --git a/letsencrypt/tests/test_util.py b/letsencrypt/tests/test_util.py deleted file mode 120000 index 80d26cbe8..000000000 --- a/letsencrypt/tests/test_util.py +++ /dev/null @@ -1 +0,0 @@ -../../acme/acme/test_util.py \ No newline at end of file diff --git a/letsencrypt/tests/test_util.py b/letsencrypt/tests/test_util.py new file mode 100644 index 000000000..2b4c6e00c --- /dev/null +++ b/letsencrypt/tests/test_util.py @@ -0,0 +1,67 @@ +"""Test utilities. + +.. warning:: This module is not part of the public API. + +""" +import os +import pkg_resources + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +import OpenSSL + +from acme import jose + + +def vector_path(*names): + """Path to a test vector.""" + return pkg_resources.resource_filename( + __name__, os.path.join('testdata', *names)) + + +def load_vector(*names): + """Load contents of a test vector.""" + # luckily, resource_string opens file in binary mode + return pkg_resources.resource_string( + __name__, os.path.join('testdata', *names)) + + +def _guess_loader(filename, loader_pem, loader_der): + _, ext = os.path.splitext(filename) + if ext.lower() == '.pem': + return loader_pem + elif ext.lower() == '.der': + return loader_der + else: # pragma: no cover + raise ValueError("Loader could not be recognized based on extension") + + +def load_cert(*names): + """Load certificate.""" + loader = _guess_loader( + names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) + return jose.ComparableX509(OpenSSL.crypto.load_certificate( + loader, load_vector(*names))) + + +def load_csr(*names): + """Load certificate request.""" + loader = _guess_loader( + names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) + return jose.ComparableX509(OpenSSL.crypto.load_certificate_request( + loader, load_vector(*names))) + + +def load_rsa_private_key(*names): + """Load RSA private key.""" + loader = _guess_loader(names[-1], serialization.load_pem_private_key, + serialization.load_der_private_key) + return jose.ComparableRSAKey(loader( + load_vector(*names), password=None, backend=default_backend())) + + +def load_pyopenssl_private_key(*names): + """Load pyOpenSSL private key.""" + loader = _guess_loader( + names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) + return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) From 7d3a49b9e73017aa07d0a8ea39ac5dad9b1febca Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 06:54:31 +0000 Subject: [PATCH 055/145] Add twine and wheel to [dev] --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 0fed79354..b1a5ada25 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,8 @@ dev_extras = [ # Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289 'astroid==1.3.5', 'pylint==1.4.2', # upstream #248 + 'twine', + 'wheel', ] docs_extras = [ From 1d5e1ee37e8d3710658b66506f35994953d362f6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 09:42:17 +0000 Subject: [PATCH 056/145] Fix letshelp_letsencrypt MANIFEST --- letshelp-letsencrypt/MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letshelp-letsencrypt/MANIFEST.in b/letshelp-letsencrypt/MANIFEST.in index 380b80fcb..96c1d7ba5 100644 --- a/letshelp-letsencrypt/MANIFEST.in +++ b/letshelp-letsencrypt/MANIFEST.in @@ -1,3 +1,3 @@ include LICENSE.txt include README.rst -recursive-include letshelp-letsencrypt/testdata * +recursive-include letshelp_letsencrypt/testdata * From 3c08b512c3b546528880a1353f6b8607307533a3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 26 Sep 2015 23:56:18 +0000 Subject: [PATCH 057/145] Simple dev release script --- .gitignore | 6 +-- tools/dev-release.sh | 96 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100755 tools/dev-release.sh diff --git a/.gitignore b/.gitignore index 8afb61ffc..ba843d9cc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,9 @@ *.egg-info/ .eggs/ build/ -dist/ -/venv/ -/venv3/ +dist*/ +/venv*/ +/kgs/ /.tox/ letsencrypt.log diff --git a/tools/dev-release.sh b/tools/dev-release.sh new file mode 100755 index 000000000..06f49f0a5 --- /dev/null +++ b/tools/dev-release.sh @@ -0,0 +1,96 @@ +#!/bin/sh -xe +# Release dev packages to PyPI + +version="0.0.0.dev$(date +%Y%m%d)" +DEV_RELEASE_BRANCH="dev-release" +# TODO: create a real release key instead of using Kuba's personal one +RELEASE_GPG_KEY="${RELEASE_GPG_KEY:-148C30F6F7E429337A72D992B00B9CC82D7ADF2C}" + +# port for a local Python Package Index (used in testing) +PORT=${PORT:-1234} + +# subpackages to be released +SUBPKGS=${SUBPKGS:-"acme letsencrypt_apache letsencrypt_nginx letshelp_letsencrypt"} +subpkgs_dirs="$(echo $SUBPKGS | sed s/_/-/g)" +# letsencrypt_compatibility_test is not packaged because: +# - it is not meant to be used by anyone else than Let's Encrypt devs +# - it causes problems when running nosetests - the latter tries to +# run everything that matches test*, while there are no unittests +# there + +tag="v$version" +mv "dist.$version" "dist.$version.$(date +%s).bak" || true +git tag --delete "$tag" || true + +root="$(mktemp -d -t le.$version.XXX)" +echo "Cloning into fresh copy at $root" # clean repo = no artificats +git clone . $root +cd $root +git branch -f "$DEV_RELEASE_BRANCH" +git checkout "$DEV_RELEASE_BRANCH" + +for pkg_dir in $subpkgs_dirs +do + sed -i $x "s/^version.*/version = '$version'/" $pkg_dir/setup.py +done +sed -i "s/^__version.*/__version__ = '$version'/" letsencrypt/__init__.py + +git add -p # interactive user input +git -c commit.gpgsign=true commit -m "Release $version" +git tag --local-user "$RELEASE_GPG_KEY" \ + --sign --message "Release $version" "$tag" + +echo "Preparing sdists and wheels" +for pkg_dir in . $subpkgs_dirs +do + cd $pkg_dir + + python setup.py clean + rm -rf build dist + python setup.py sdist + python setup.py bdist_wheel + + echo "Signing ($pkg_dir)" + for x in dist/*.tar.gz dist/*.whl + do + gpg2 --detach-sign --armor --sign $x + done + + cd - +done + +mkdir "dist.$version" +mv dist "dist.$version/letsencrypt" +for pkg_dir in $subpkgs_dirs +do + mv $pkg_dir/dist "dist.$version/$pkg_dir/" +done + +echo "Testing packages" +cd "dist.$version" +# start local PyPI +python -m SimpleHTTPServer $PORT & +# cd .. is NOT done on purpose: we make sure that all subpacakges are +# installed from local PyPI rather than current directory (repo root) +virtualenv --no-site-packages ../venv +. ../venv/bin/activate +# Now, use our local PyPI. --pre allows installation of pre-release (incl. dev) +pip install \ + --pre \ + --extra-index-url http://localhost:$PORT \ + letsencrypt $SUBPKGS +# stop local PyPI +kill $! + +# freeze before installing anythin else, so that we know end-user KGS +mkdir kgs +kgs="kgs/$version" +pip freeze | tee $kgs +pip install nose +# TODO: letsencrypt_apache fails due to symlink, c.f. #838 +nosetests letsencrypt $SUBPKGS || true + +echo "New root: $root" +echo "KGS is at $root/$kgs" +echo "In order to upload packages run the following command:" +echo twine upload "$root/dist.$version/*/*" From cbfdae88fcde764b0a60190d12881c9945fe2437 Mon Sep 17 00:00:00 2001 From: Brandon Kreisel Date: Sun, 27 Sep 2015 14:44:00 -0400 Subject: [PATCH 058/145] Add Mac compatibility to boulder-start The version of sort that ships with OS X does not support the -V version flag. Emulate that functionality with some sed-fu --- tests/boulder-start.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/boulder-start.sh b/tests/boulder-start.sh index 7ce7dcba4..e8c50633f 100755 --- a/tests/boulder-start.sh +++ b/tests/boulder-start.sh @@ -4,11 +4,16 @@ # ugh, go version output is like: # go version go1.4.2 linux/amd64 -GOVER=`go version | cut -d" " -f3 | cut -do -f2` +GOVER=`go version | cut -d" " -f3 | cut -do -f2` # version comparison function verlte { + if [ `uname` == 'Darwin' ]; then + [ "$1" = "`echo -e \"$1\n$2\" | sed 's/\b\([0-9]\)\b/0\1/g' \ + | sort | sed 's/\b0\([0-9]\)/\1/g' | head -n1`" ] + else [ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ] + fi } if ! verlte 1.5 "$GOVER" ; then From 6649af94790407a1d0a2bde72cc927ca881a64ad Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 20:57:31 +0000 Subject: [PATCH 059/145] Developer virtualenv bootstrap scripts. --- bootstrap/dev/README | 1 + bootstrap/dev/_venv_common.sh | 25 +++++++++++++++ bootstrap/dev/venv.sh | 11 +++++++ bootstrap/dev/venv3.sh | 8 +++++ docs/contributing.rst | 59 ++++++++++++++++++----------------- docs/using.rst | 2 ++ 6 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 bootstrap/dev/README create mode 100755 bootstrap/dev/_venv_common.sh create mode 100755 bootstrap/dev/venv.sh create mode 100755 bootstrap/dev/venv3.sh diff --git a/bootstrap/dev/README b/bootstrap/dev/README new file mode 100644 index 000000000..1509c24e0 --- /dev/null +++ b/bootstrap/dev/README @@ -0,0 +1 @@ +This directory contains developer setup. \ No newline at end of file diff --git a/bootstrap/dev/_venv_common.sh b/bootstrap/dev/_venv_common.sh new file mode 100755 index 000000000..2d84dc39b --- /dev/null +++ b/bootstrap/dev/_venv_common.sh @@ -0,0 +1,25 @@ +#!/bin/sh -xe + +VENV_NAME=${VENV_NAME:-venv} + +# .egg-info directories tend to cause bizzaire problems (e.g. `pip -e +# .` might unexpectedly install letshelp-letsencrypt only, in case +# `python letshelp-letsencrypt/setup.py build` has been called +# earlier) +rm -rf *.egg-info + +# virtualenv setup is NOT idempotent: shutil.Error: +# `/home/jakub/dev/letsencrypt/letsencrypt/venv/bin/python2` and +# `venv/bin/python2` are the same file +mv $VENV_NAME "$VENV_NAME.$(date +%s).bak" || true +virtualenv --no-site-packages $VENV_NAME $VENV_ARGS +. ./$VENV_NAME/bin/activate + +# Separately install setuptools and pip to make sure following +# invocations use latest +pip install -U setuptools +pip install -U pip +pip install "$@" + +echo "Please run the following command to activate developer environment:" +echo "source $VENV_NAME/bin/activate" diff --git a/bootstrap/dev/venv.sh b/bootstrap/dev/venv.sh new file mode 100755 index 000000000..90088ac9b --- /dev/null +++ b/bootstrap/dev/venv.sh @@ -0,0 +1,11 @@ +#!/bin/sh -xe +# Developer virtualenv setup for Let's Encrypt client + +./bootstrap/dev/_venv_common.sh \ + -r requirements.txt \ + -e acme[testing] \ + -e .[dev,docs,testing] \ + -e letsencrypt-apache \ + -e letsencrypt-nginx \ + -e letshelp-letsencrypt \ + -e letsencrypt-compatibility-test diff --git a/bootstrap/dev/venv3.sh b/bootstrap/dev/venv3.sh new file mode 100755 index 000000000..ccffffb83 --- /dev/null +++ b/bootstrap/dev/venv3.sh @@ -0,0 +1,8 @@ +#!/bin/sh -xe +# Developer Python3 virtualenv setup for Let's Encrypt + +export VENV_NAME="${VENV_NAME:-venv3}" +export VENV_ARGS="--python python3" + +./bootstrap/dev/_venv_common.sh \ + -e acme[testing] \ diff --git a/docs/contributing.rst b/docs/contributing.rst index c6443e3b2..6a9682494 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -7,39 +7,40 @@ Contributing Hacking ======= -Start by :doc:`installing dependencies and setting up Let's Encrypt -`. - -When you're done activate the virtualenv: - -.. code-block:: shell - - source ./venv/bin/activate - -This step should prepend you prompt with ``(venv)`` and save you from -typing ``./venv/bin/...``. It is also required to run some of the -`testing`_ tools. Virtualenv can be disabled at any time by typing -``deactivate``. More information can be found in `virtualenv -documentation`_. - -Install the development packages: - -.. code-block:: shell - - pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt - -.. note:: `-e` (short for `--editable`) turns on *editable mode* in - which any source code changes in the current working - directory are "live" and no further `pip install ...` - invocations are necessary while developing. - - This is roughly equivalent to `python setup.py develop`. For - more info see `man pip`. - The code base, including your pull requests, **must** have 100% unit test coverage, pass our `integration`_ tests **and** be compliant with the :ref:`coding style `. + +Bootstrap +--------- + +Start by :ref:`installing Let's Encrypt prerequisites +`. Then run: + +.. code-block:: shell + + ./bootstrap/dev/venv.sh + ./bootstrap/dev/venv3.sh + +Both of the commands suggest to activate the virtualenv (you can +activate one at a time only): + +.. code-block:: shell + + source ./$VENV_NAME/bin/activate + +This step should prepend you prompt with ``($VENV_NAME)`` and save you +from typing ``./$VENV_NAME/bin/...``. It is also required to run some +of the `testing`_ tools. Virtualenv can be disabled at any time by +typing ``deactivate``. More information can be found in `virtualenv +documentation`_. + +Note that packages are installed in so called *editable mode*, in +which any source code changes in the current working directory are +"live" and no further ``./bootstrap/dev/venv.sh`` or ``pip install +...`` invocations are necessary while developing. + .. _`virtualenv documentation`: https://virtualenv.pypa.io diff --git a/docs/using.rst b/docs/using.rst index cfce29bae..53ecd72b5 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -42,6 +42,8 @@ above method instead. https://github.com/letsencrypt/letsencrypt/archive/master.zip +.. _prerequisites: + Prerequisites ============= From 85e5165b5d82f3700ba4c960b3f957f7c4d7d3d9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 20:59:07 +0000 Subject: [PATCH 060/145] nit: missing EOF newline --- bootstrap/dev/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/dev/README b/bootstrap/dev/README index 1509c24e0..759496187 100644 --- a/bootstrap/dev/README +++ b/bootstrap/dev/README @@ -1 +1 @@ -This directory contains developer setup. \ No newline at end of file +This directory contains developer setup. From 5d8e9a3d68b362634c9fb752e5a0bcb4fb12d021 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 21:07:40 +0000 Subject: [PATCH 061/145] Fix various doc generation issues --- acme/acme/challenges.py | 2 +- docs/api/display.rst | 6 ------ docs/api/recovery_token.rst | 5 ----- docs/api/revoker.rst | 5 ----- letsencrypt-apache/letsencrypt_apache/configurator.py | 2 +- 5 files changed, 2 insertions(+), 18 deletions(-) delete mode 100644 docs/api/recovery_token.rst delete mode 100644 docs/api/revoker.rst diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 13186cc4f..81711e605 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -542,7 +542,7 @@ class DNS(DVChallenge): def check_validation(self, validation, account_public_key): """Check validation. - :param validation + :param JWS validation: :type account_public_key: `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` or diff --git a/docs/api/display.rst b/docs/api/display.rst index b79ef25d7..117a91708 100644 --- a/docs/api/display.rst +++ b/docs/api/display.rst @@ -21,9 +21,3 @@ .. automodule:: letsencrypt.display.enhancements :members: - -:mod:`letsencrypt.display.revocation` -===================================== - -.. automodule:: letsencrypt.display.revocation - :members: diff --git a/docs/api/recovery_token.rst b/docs/api/recovery_token.rst deleted file mode 100644 index 774aa4b3c..000000000 --- a/docs/api/recovery_token.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.recovery_token` --------------------------------------------------- - -.. automodule:: letsencrypt.recovery_token - :members: diff --git a/docs/api/revoker.rst b/docs/api/revoker.rst deleted file mode 100644 index a482a138e..000000000 --- a/docs/api/revoker.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.revoker` --------------------------- - -.. automodule:: letsencrypt.revoker - :members: diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index f301de8b9..ad3c62d2c 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -1162,7 +1162,7 @@ def _get_mod_deps(mod_name): changes. .. warning:: If all deps are not included, it may cause incorrect parsing behavior, due to enable_mod's shortcut for updating the parser's - currently defined modules (:method:`.ApacheConfigurator._add_parser_mod`) + currently defined modules (`.ApacheConfigurator._add_parser_mod`) This would only present a major problem in extremely atypical configs that use ifmod for the missing deps. From 96a737bbbaf9aa76accdbd9421b19e38a0703e72 Mon Sep 17 00:00:00 2001 From: David Xia Date: Sun, 27 Sep 2015 16:51:20 -0400 Subject: [PATCH 062/145] Fix CLI --help for OS X OS X's signal module doesn't have SIGPWR. Don't try to use it. Fixes #841 --- letsencrypt/error_handler.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py index fedb66c0e..99f502ac2 100644 --- a/letsencrypt/error_handler.py +++ b/letsencrypt/error_handler.py @@ -2,6 +2,7 @@ import logging import os import signal +import sys import traceback @@ -13,9 +14,14 @@ logger = logging.getLogger(__name__) # potentially occur from inside Python. Signals such as SIGILL were not # included as they could be a sign of something devious and we should terminate # immediately. -_SIGNALS = ([signal.SIGTERM] if os.name == "nt" else - [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, - signal.SIGXCPU, signal.SIGXFSZ, signal.SIGPWR]) +if os.name == "nt": + _SIGNALS = [signal.SIGTERM] +elif sys.platform == "darwin": + _SIGNALS = [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, signal.SIGXCPU, + signal.SIGXFSZ] +else: + _SIGNALS = [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, signal.SIGXCPU, + signal.SIGXFSZ, signal.SIGPWR] class ErrorHandler(object): From a7375eb5494df494d2604ee1e903467b093af30b Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Sun, 27 Sep 2015 17:44:31 -0700 Subject: [PATCH 063/145] Emit error when simple_verify fails. When running the manual authenticator, if simple_verify fails, there is no output to indicate what went wrong, just "Incomplete authorizations." --- letsencrypt/plugins/manual.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 2014c8c0e..2fad4ac53 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -182,6 +182,8 @@ binary for temporary key/certificate generation.""".replace("\n", "") achall.account_key.public_key(), self.config.simple_http_port): return response else: + logger.error( + "Self-verify of challenge failed, authorization abandoned.\n") if self.conf("test-mode") and self._httpd.poll() is not None: # simply verify cause command failure... return False From 913a0a9e98b2559ab960b58dd533a932cdde8150 Mon Sep 17 00:00:00 2001 From: Jadaw1n Date: Mon, 28 Sep 2015 17:34:43 +0200 Subject: [PATCH 064/145] Dockerfile: option --text doesn't exist --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 789e26af9..b9ea168de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,5 +62,5 @@ RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ # bash" and investigate, apply patches, etc. ENV PATH /opt/letsencrypt/venv/bin:$PATH -# TODO: is --text really necessary? -ENTRYPOINT [ "letsencrypt", "--text" ] + +ENTRYPOINT [ "letsencrypt" ] From 27268afdcc82a34e0d37d39bd6a14af5431ddb8c Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 28 Sep 2015 11:58:12 -0700 Subject: [PATCH 065/145] Remove extra newline. --- letsencrypt/plugins/manual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 2fad4ac53..3f7276725 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -183,7 +183,7 @@ binary for temporary key/certificate generation.""".replace("\n", "") return response else: logger.error( - "Self-verify of challenge failed, authorization abandoned.\n") + "Self-verify of challenge failed, authorization abandoned.") if self.conf("test-mode") and self._httpd.poll() is not None: # simply verify cause command failure... return False From 315b3577811fba3d3a540c22cc2f6bf772fb98af Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 27 Sep 2015 21:27:36 +0000 Subject: [PATCH 066/145] Hide null installer (fixes #789). --- letsencrypt/cli.py | 2 +- letsencrypt/display/ops.py | 2 +- letsencrypt/plugins/disco.py | 9 +++++++++ letsencrypt/plugins/null.py | 1 + letsencrypt/tests/display/ops_test.py | 12 +++++++----- setup.py | 1 - 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 3317ae549..8bcbd8f02 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -420,7 +420,7 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print logger.debug("Expected interfaces: %s", args.ifaces) ifaces = [] if args.ifaces is None else args.ifaces - filtered = plugins.ifaces(ifaces) + filtered = plugins.visible().ifaces(ifaces) logger.debug("Filtered plugins: %r", filtered) if not args.init and not args.prepare: diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 4ab3ec579..43705e309 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -65,7 +65,7 @@ def pick_plugin(config, default, plugins, question, ifaces): # throw more UX-friendly error if default not in plugins filtered = plugins.filter(lambda p_ep: p_ep.name == default) else: - filtered = plugins.ifaces(ifaces) + filtered = plugins.visible().ifaces(ifaces) filtered.init(config) verified = filtered.verify(ifaces) diff --git a/letsencrypt/plugins/disco.py b/letsencrypt/plugins/disco.py index b6cdb1f99..5a41fda88 100644 --- a/letsencrypt/plugins/disco.py +++ b/letsencrypt/plugins/disco.py @@ -50,6 +50,11 @@ class PluginEntryPoint(object): """Description with name. Handy for UI.""" return "{0} ({1})".format(self.description, self.name) + @property + def hidden(self): + """Should this plugin be hidden from UI?""" + return getattr(self.plugin_cls, "hidden", False) + def ifaces(self, *ifaces_groups): """Does plugin implements specified interface groups?""" return not ifaces_groups or any( @@ -183,6 +188,10 @@ class PluginsRegistry(collections.Mapping): return type(self)(dict((name, plugin_ep) for name, plugin_ep in self._plugins.iteritems() if pred(plugin_ep))) + def visible(self): + """Filter plugins based on visibility.""" + return self.filter(lambda plugin_ep: not plugin_ep.hidden) + def ifaces(self, *ifaces_groups): """Filter plugins based on interfaces.""" # pylint: disable=star-args diff --git a/letsencrypt/plugins/null.py b/letsencrypt/plugins/null.py index efe041cac..4ba6c9d64 100644 --- a/letsencrypt/plugins/null.py +++ b/letsencrypt/plugins/null.py @@ -17,6 +17,7 @@ class Installer(common.Plugin): zope.interface.classProvides(interfaces.IPluginFactory) description = "Null Installer" + hidden = True # pylint: disable=missing-docstring,no-self-use diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 7420a62f0..9d4a3a933 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -84,7 +84,7 @@ class PickPluginTest(unittest.TestCase): def test_no_default(self): self._call() - self.assertEqual(1, self.reg.ifaces.call_count) + self.assertEqual(1, self.reg.visible().ifaces.call_count) def test_no_candidate(self): self.assertTrue(self._call() is None) @@ -94,7 +94,8 @@ class PickPluginTest(unittest.TestCase): plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = False - self.reg.ifaces().verify().available.return_value = {"bar": plugin_ep} + self.reg.visible().ifaces().verify().available.return_value = { + "bar": plugin_ep} self.assertEqual("foo", self._call()) def test_single_misconfigured(self): @@ -102,13 +103,14 @@ class PickPluginTest(unittest.TestCase): plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = True - self.reg.ifaces().verify().available.return_value = {"bar": plugin_ep} + self.reg.visible().ifaces().verify().available.return_value = { + "bar": plugin_ep} self.assertTrue(self._call() is None) def test_multiple(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" - self.reg.ifaces().verify().available.return_value = { + self.reg.visible().ifaces().verify().available.return_value = { "bar": plugin_ep, "baz": plugin_ep, } @@ -119,7 +121,7 @@ class PickPluginTest(unittest.TestCase): [plugin_ep, plugin_ep], self.question) def test_choose_plugin_none(self): - self.reg.ifaces().verify().available.return_value = { + self.reg.visible().ifaces().verify().available.return_value = { "bar": None, "baz": None, } diff --git a/setup.py b/setup.py index c568d2872..8f75aff03 100644 --- a/setup.py +++ b/setup.py @@ -118,7 +118,6 @@ setup( ], 'letsencrypt.plugins': [ 'manual = letsencrypt.plugins.manual:Authenticator', - # TODO: null should probably not be presented to the user 'null = letsencrypt.plugins.null:Installer', 'standalone = letsencrypt.plugins.standalone.authenticator' ':StandaloneAuthenticator', From c1012f5f0082dd99d22fb5a49695dfbdfd433f19 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 28 Sep 2015 12:25:37 -0700 Subject: [PATCH 067/145] Removed SIGPWR entirely --- letsencrypt/error_handler.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py index 99f502ac2..1f979a6de 100644 --- a/letsencrypt/error_handler.py +++ b/letsencrypt/error_handler.py @@ -2,7 +2,6 @@ import logging import os import signal -import sys import traceback @@ -14,14 +13,9 @@ logger = logging.getLogger(__name__) # potentially occur from inside Python. Signals such as SIGILL were not # included as they could be a sign of something devious and we should terminate # immediately. -if os.name == "nt": - _SIGNALS = [signal.SIGTERM] -elif sys.platform == "darwin": - _SIGNALS = [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, signal.SIGXCPU, - signal.SIGXFSZ] -else: - _SIGNALS = [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, signal.SIGXCPU, - signal.SIGXFSZ, signal.SIGPWR] +_SIGNALS = ([signal.SIGTERM] if os.name == "nt" else + [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, + signal.SIGXCPU, signal.SIGXFSZ]) class ErrorHandler(object): From ab98d5c39fc19cc90785a87f10cc4b53390e8b20 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Mon, 28 Sep 2015 17:14:33 -0400 Subject: [PATCH 068/145] Ignore unknown challenge types --- acme/acme/messages.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 02ae24c8f..002c08767 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -373,7 +373,17 @@ class Authorization(ResourceBody): @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(ChallengeBody.from_json(chall) for chall in value) + # The from_json method raises errors.UnrecognizedTypeError when a + # challenge of unknown type is encountered. We want to ignore this + # case. This forces us to do an explicit iteration, since list + # comprehensions can't handle exceptions. + challenges = [] + for chall in value: + try: + challenges.append(ChallengeBody.from_json(chall)) + except errors.UnknownTypeError: + continue + return tuple(challenges) @property def resolved_combinations(self): From b6bbc9e0a29a7b64ecc03b1ffbbccf67cac37238 Mon Sep 17 00:00:00 2001 From: Brandon Kreisel Date: Mon, 28 Sep 2015 17:39:01 -0400 Subject: [PATCH 069/145] Add inline Mac comment --- tests/boulder-start.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/boulder-start.sh b/tests/boulder-start.sh index e8c50633f..530f9c598 100755 --- a/tests/boulder-start.sh +++ b/tests/boulder-start.sh @@ -8,6 +8,7 @@ GOVER=`go version | cut -d" " -f3 | cut -do -f2` # version comparison function verlte { + #OS X doesn't support version sorting; emulate with sed if [ `uname` == 'Darwin' ]; then [ "$1" = "`echo -e \"$1\n$2\" | sed 's/\b\([0-9]\)\b/0\1/g' \ | sort | sed 's/\b0\([0-9]\)/\1/g' | head -n1`" ] From 3279aefefbd409aae2f1bb954cd67d266240e973 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 28 Sep 2015 15:15:44 -0700 Subject: [PATCH 070/145] Made PEP8 happy --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8bcbd8f02..dccfb9289 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -518,7 +518,7 @@ class HelpfulArgumentParser(object): help2 = self.prescan_for_flag("--help", self.help_topics) assert max(True, "a") == "a", "Gravity changed direction" help_arg = max(help1, help2) - if help_arg == True: + if help_arg is True: # just --help with no topic; avoid argparse altogether print USAGE sys.exit(0) From fa992faf52be93309506ae728eb64340fd388706 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 28 Sep 2015 15:24:51 -0700 Subject: [PATCH 071/145] Fix pylint and add test --- acme/acme/messages.py | 11 ++++++----- acme/acme/messages_test.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 002c08767..594b3d5c7 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -2,6 +2,7 @@ import collections from acme import challenges +from acme import errors from acme import fields from acme import jose from acme import util @@ -373,17 +374,17 @@ class Authorization(ResourceBody): @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument - # The from_json method raises errors.UnrecognizedTypeError when a + # The from_json method raises errors.UnrecognizedTypeError when a # challenge of unknown type is encountered. We want to ignore this # case. This forces us to do an explicit iteration, since list # comprehensions can't handle exceptions. - challenges = [] + challs = [] for chall in value: try: - challenges.append(ChallengeBody.from_json(chall)) - except errors.UnknownTypeError: + challs.append(ChallengeBody.from_json(chall)) + except jose.UnrecognizedTypeError: continue - return tuple(challenges) + return tuple(challs) @property def resolved_combinations(self): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index 25f07018c..ac722909c 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -274,6 +274,9 @@ class AuthorizationTest(unittest.TestCase): def setUp(self): from acme.messages import ChallengeBody from acme.messages import STATUS_VALID + + unknown_chall = mock.MagicMock() + unknown_chall.to_json.side_effect = side_effect=jose.UnrecognizedTypeError self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, @@ -300,6 +303,19 @@ class AuthorizationTest(unittest.TestCase): 'combinations': combinations, } + # For unknown challenge types + self.jmsg_unknown_chall = { + 'resource': 'challenge', + 'uri': 'random_uri', + 'type': 'unknown', + 'tls': True, + } + + self.jobj_from_unknown = { + 'identifier': identifier.to_json(), + 'challenges': [self.jmsg_unknown_chall], + } + def test_from_json(self): from acme.messages import Authorization Authorization.from_json(self.jobj_from) @@ -314,6 +330,11 @@ class AuthorizationTest(unittest.TestCase): (self.challbs[1], self.challbs[2]), )) + def test_unknown_chall_type(self): + """Just make sure an error isn't thrown.""" + from acme.messages import Authorization + Authorization.from_json(self.jobj_from_unknown) + class AuthorizationResourceTest(unittest.TestCase): """Tests for acme.messages.AuthorizationResource.""" From 4da0e17255a15d0e9589795410b25c05a6b87cc2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 28 Sep 2015 15:45:31 -0700 Subject: [PATCH 072/145] Added message and changed reporter interface --- letsencrypt/account.py | 4 ++-- letsencrypt/auth_handler.py | 2 +- letsencrypt/cli.py | 11 +++++++++++ letsencrypt/client.py | 2 +- letsencrypt/interfaces.py | 2 +- letsencrypt/reporter.py | 2 +- letsencrypt/tests/reporter_test.py | 6 +++--- 7 files changed, 20 insertions(+), 9 deletions(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 8bee22102..c97e4f6fe 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -92,13 +92,13 @@ def report_new_account(acc, config): "contain certificates and private keys obtained by Let's Encrypt " "so making regular backups of this folder is ideal.".format( config.config_dir), - reporter.MEDIUM_PRIORITY, True) + reporter.MEDIUM_PRIORITY) if acc.regr.body.emails: recovery_msg = ("If you lose your account credentials, you can " "recover through e-mails sent to {0}.".format( ", ".join(acc.regr.body.emails))) - reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True) + reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY) class AccountMemoryStorage(interfaces.AccountStorage): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 68aed510a..b27a569f6 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -531,7 +531,7 @@ def _report_failed_challs(failed_achalls): reporter = zope.component.getUtility(interfaces.IReporter) for achalls in problems.itervalues(): reporter.add_message( - _generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY, True) + _generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY) def _generate_failed_chall_msg(failed_achalls): diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index dccfb9289..bd49d110b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -267,6 +267,14 @@ def _treat_as_renewal(config, domains): return None +def _report_new_cert(cert_path): + """Reports the creation of a new certificate to the user.""" + reporter_util = zope.component.getUtility(interfaces.IReporter) + reporter_util.add_message("Congratulations! Your certificate has been " + "saved at {0}.".format(cert_path), + reporter.MEDIUM_PRIORITY) + + def _auth_from_domains(le_client, config, domains, plugins): """Authenticate and enroll certificate.""" # Note: This can raise errors... caught above us though. @@ -292,6 +300,8 @@ def _auth_from_domains(le_client, config, domains, plugins): if not lineage: raise errors.Error("Certificate could not be obtained") + _report_new_cert(lineage.cert) + return lineage @@ -365,6 +375,7 @@ def auth(args, config, plugins): file=args.csr[0], data=args.csr[1], form="der")) le_client.save_certificate( certr, chain, args.cert_path, args.chain_path) + _report_new_cert(args.cert_path) else: domains = _find_domains(args, installer) _auth_from_domains(le_client, config, domains, plugins) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index e9decae47..c82131af3 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -286,7 +286,7 @@ class Client(object): "configured in the directories under {0}.").format( cert.cli_config.renewal_configs_dir) reporter = zope.component.getUtility(interfaces.IReporter) - reporter.add_message(msg, reporter.LOW_PRIORITY, True) + reporter.add_message(msg, reporter.LOW_PRIORITY) def save_certificate(self, certr, chain_cert, cert_path, chain_path): # pylint: disable=no-self-use diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 1ba8afe45..1f51645ab 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -478,7 +478,7 @@ class IReporter(zope.interface.Interface): LOW_PRIORITY = zope.interface.Attribute( "Used to denote low priority messages") - def add_message(self, msg, priority, on_crash=False): + def add_message(self, msg, priority, on_crash=True): """Adds msg to the list of messages to be printed. :param str msg: Message to be displayed to the user. diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 482305838..0905dfa54 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -36,7 +36,7 @@ class Reporter(object): def __init__(self): self.messages = Queue.PriorityQueue() - def add_message(self, msg, priority, on_crash=False): + def add_message(self, msg, priority, on_crash=True): """Adds msg to the list of messages to be printed. :param str msg: Message to be displayed to the user. diff --git a/letsencrypt/tests/reporter_test.py b/letsencrypt/tests/reporter_test.py index c43511208..89bd9dfc7 100644 --- a/letsencrypt/tests/reporter_test.py +++ b/letsencrypt/tests/reporter_test.py @@ -78,13 +78,13 @@ class ReporterTest(unittest.TestCase): output = sys.stdout.getvalue() self.assertTrue("IMPORTANT NOTES:" in output) self.assertTrue("High" in output) - self.assertTrue("Med" not in output) + self.assertTrue("Med" in output) self.assertTrue("Low" not in output) def _add_messages(self): - self.reporter.add_message("High", self.reporter.HIGH_PRIORITY, True) + self.reporter.add_message("High", self.reporter.HIGH_PRIORITY) self.reporter.add_message("Med", self.reporter.MEDIUM_PRIORITY) - self.reporter.add_message("Low", self.reporter.LOW_PRIORITY) + self.reporter.add_message("Low", self.reporter.LOW_PRIORITY, False) if __name__ == "__main__": From 243c9e9021cd1183742a516aed0a432a9cc65b73 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 28 Sep 2015 15:52:09 -0700 Subject: [PATCH 073/145] Made cover and lint happy --- letsencrypt/cli.py | 2 +- letsencrypt/tests/reporter_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index bd49d110b..0b7d17909 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -272,7 +272,7 @@ def _report_new_cert(cert_path): reporter_util = zope.component.getUtility(interfaces.IReporter) reporter_util.add_message("Congratulations! Your certificate has been " "saved at {0}.".format(cert_path), - reporter.MEDIUM_PRIORITY) + reporter_util.MEDIUM_PRIORITY) def _auth_from_domains(le_client, config, domains, plugins): diff --git a/letsencrypt/tests/reporter_test.py b/letsencrypt/tests/reporter_test.py index 89bd9dfc7..ddf345c4c 100644 --- a/letsencrypt/tests/reporter_test.py +++ b/letsencrypt/tests/reporter_test.py @@ -78,12 +78,12 @@ class ReporterTest(unittest.TestCase): output = sys.stdout.getvalue() self.assertTrue("IMPORTANT NOTES:" in output) self.assertTrue("High" in output) - self.assertTrue("Med" in output) + self.assertTrue("Med" not in output) self.assertTrue("Low" not in output) def _add_messages(self): self.reporter.add_message("High", self.reporter.HIGH_PRIORITY) - self.reporter.add_message("Med", self.reporter.MEDIUM_PRIORITY) + self.reporter.add_message("Med", self.reporter.MEDIUM_PRIORITY, False) self.reporter.add_message("Low", self.reporter.LOW_PRIORITY, False) From 67ec4d09eef289b979f18b869c760cc997ef2f44 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 28 Sep 2015 15:53:42 -0700 Subject: [PATCH 074/145] Put in dummy challenge --- acme/acme/challenges.py | 5 +++++ acme/acme/messages.py | 5 +++-- acme/acme/messages_test.py | 2 -- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 81711e605..1ffc6cc99 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -34,6 +34,11 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method """Domain validation challenges.""" +class UnrecognizedChallenge(DVChallenge): + """Unrecognized challenge.""" + typ = "unknown" + + class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge response.""" diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 594b3d5c7..d6e9952c3 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -2,7 +2,6 @@ import collections from acme import challenges -from acme import errors from acme import fields from acme import jose from acme import util @@ -383,7 +382,9 @@ class Authorization(ResourceBody): try: challs.append(ChallengeBody.from_json(chall)) except jose.UnrecognizedTypeError: - continue + challs.append(ChallengeBody( + uri="UNKNOWN", chall=challenges.UnrecognizedChallenge, + status=STATUS_UNKNOWN)) return tuple(challs) @property diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index ac722909c..d7bbdb0e4 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -275,8 +275,6 @@ class AuthorizationTest(unittest.TestCase): from acme.messages import ChallengeBody from acme.messages import STATUS_VALID - unknown_chall = mock.MagicMock() - unknown_chall.to_json.side_effect = side_effect=jose.UnrecognizedTypeError self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, From 5238f530924de2bf335b958a102b31306cf4a79d Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 28 Sep 2015 16:03:03 -0700 Subject: [PATCH 075/145] DVChallenge -> Challenge --- 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 1ffc6cc99..fbb2e7418 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -34,7 +34,7 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method """Domain validation challenges.""" -class UnrecognizedChallenge(DVChallenge): +class UnrecognizedChallenge(Challenge): """Unrecognized challenge.""" typ = "unknown" From ed7977fb039d74455d088a4bb11cbf2eaf91373b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 28 Sep 2015 18:45:12 -0700 Subject: [PATCH 076/145] Added cli tests --- letsencrypt/tests/cli_test.py | 140 ++++++++++++++++++++++++++-------- 1 file changed, 107 insertions(+), 33 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 2e9f3330c..31cef584b 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -16,6 +16,9 @@ from letsencrypt.tests import renewer_test from letsencrypt.tests import test_util +CSR = test_util.vector_path('csr.der') + + class CLITest(unittest.TestCase): """Tests for different commands.""" @@ -65,40 +68,111 @@ class CLITest(unittest.TestCase): for r in xrange(len(flags)))): self._call(['plugins'] + list(args)) - @mock.patch("letsencrypt.cli.sys") + def test_auth_bad_args(self): + ret, _, _, _ = self._call(['-d', 'foo.bar', 'auth', '--csr', CSR]) + self.assertEqual(ret, '--domains and --csr are mutually exclusive') + + ret, _, _, _ = self._call(['-a', 'bad_auth', 'auth']) + self.assertEqual(ret, 'Authenticator could not be determined') + + @mock.patch('letsencrypt.cli.zope.component.getUtility') + def test_auth_new_request_success(self, mock_get_utility): + cert_path = '/etc/letsencrypt/live/foo.bar' + mock_lineage = mock.MagicMock(cert=cert_path) + mock_client = mock.MagicMock() + mock_client.obtain_and_enroll_certificate.return_value = mock_lineage + self._auth_new_request_common(mock_client) + self.assertEqual( + mock_client.obtain_and_enroll_certificate.call_count, 1) + self.assertTrue( + cert_path in mock_get_utility().add_message.call_args[0][0]) + + def test_auth_new_request_failure(self): + mock_client = mock.MagicMock() + mock_client.obtain_and_enroll_certificate.return_value = False + self.assertRaises(errors.Error, + self._auth_new_request_common, mock_client) + + def _auth_new_request_common(self, mock_client): + with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal: + mock_renewal.return_value = None + with mock.patch('letsencrypt.cli._init_le_client') as mock_init: + mock_init.return_value = mock_client + self._call(['-d', 'foo.bar', '-a', + 'standalone', '-i', 'bad', 'auth']) + + @mock.patch('letsencrypt.cli.zope.component.getUtility') + @mock.patch('letsencrypt.cli._treat_as_renewal') + @mock.patch('letsencrypt.cli._init_le_client') + def test_auth_renewal(self, mock_init, mock_renewal, mock_get_utility): + cert_path = '/etc/letsencrypt/live/foo.bar' + mock_lineage = mock.MagicMock(cert=cert_path) + mock_cert = mock.MagicMock(body='body') + mock_key = mock.MagicMock(pem='pem_key') + mock_renewal.return_value = mock_lineage + mock_client = mock.MagicMock() + mock_client.obtain_certificate.return_value = (mock_cert, 'chain', + mock_key, 'csr') + mock_init.return_value = mock_client + with mock.patch('letsencrypt.cli.OpenSSL'): + with mock.patch('letsencrypt.cli.crypto_util'): + self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth']) + mock_client.obtain_certificate.assert_called_once_with(['foo.bar']) + self.assertEqual(mock_lineage.save_successor.call_count, 1) + mock_lineage.update_all_links_to.assert_called_once_with( + mock_lineage.latest_common_version()) + self.assertTrue( + cert_path in mock_get_utility().add_message.call_args[0][0]) + + @mock.patch('letsencrypt.cli.zope.component.getUtility') + @mock.patch('letsencrypt.cli._init_le_client') + def test_auth_csr(self, mock_init, mock_get_utility): + cert_path = '/etc/letsencrypt/live/foo.bar' + mock_client = mock.MagicMock() + mock_client.obtain_certificate_from_csr.return_value = ('certr', + 'chain') + mock_init.return_value = mock_client + self._call(['-a', 'standalone', 'auth', '--csr', CSR, + '--cert-path', cert_path, '--chain-path', '/']) + mock_client.save_certificate.assert_called_once_with( + 'certr', 'chain', cert_path, '/') + self.assertTrue( + cert_path in mock_get_utility().add_message.call_args[0][0]) + + @mock.patch('letsencrypt.cli.sys') def test_handle_exception(self, mock_sys): # pylint: disable=protected-access from letsencrypt import cli mock_open = mock.mock_open() - with mock.patch("letsencrypt.cli.open", mock_open, create=True): - exception = Exception("detail") + with mock.patch('letsencrypt.cli.open', mock_open, create=True): + exception = Exception('detail') cli._handle_exception( Exception, exc_value=exception, trace=None, args=None) - mock_open().write.assert_called_once_with("".join( + mock_open().write.assert_called_once_with(''.join( traceback.format_exception_only(Exception, exception))) error_msg = mock_sys.exit.call_args_list[0][0][0] - self.assertTrue("unexpected error" in error_msg) + self.assertTrue('unexpected error' in error_msg) - with mock.patch("letsencrypt.cli.open", mock_open, create=True): + with mock.patch('letsencrypt.cli.open', mock_open, create=True): mock_open.side_effect = [KeyboardInterrupt] - error = errors.Error("detail") + error = errors.Error('detail') cli._handle_exception( errors.Error, exc_value=error, trace=None, args=None) # assert_any_call used because sys.exit doesn't exit in cli.py - mock_sys.exit.assert_any_call("".join( + mock_sys.exit.assert_any_call(''.join( traceback.format_exception_only(errors.Error, error))) args = mock.MagicMock(debug=False) cli._handle_exception( - Exception, exc_value=Exception("detail"), trace=None, args=args) + Exception, exc_value=Exception('detail'), trace=None, args=args) error_msg = mock_sys.exit.call_args_list[-1][0][0] - self.assertTrue("unexpected error" in error_msg) + self.assertTrue('unexpected error' in error_msg) - interrupt = KeyboardInterrupt("detail") + interrupt = KeyboardInterrupt('detail') cli._handle_exception( KeyboardInterrupt, exc_value=interrupt, trace=None, args=None) - mock_sys.exit.assert_called_with("".join( + mock_sys.exit.assert_called_with(''.join( traceback.format_exception_only(KeyboardInterrupt, interrupt))) @@ -108,13 +182,13 @@ class DetermineAccountTest(unittest.TestCase): def setUp(self): self.args = mock.MagicMock(account=None, email=None) self.config = configuration.NamespaceConfig(self.args) - self.accs = [mock.MagicMock(id="x"), mock.MagicMock(id="y")] + self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')] self.account_storage = account.AccountMemoryStorage() def _call(self): # pylint: disable=protected-access from letsencrypt.cli import _determine_account - with mock.patch("letsencrypt.cli.account.AccountFileStorage") as mock_storage: + with mock.patch('letsencrypt.cli.account.AccountFileStorage') as mock_storage: mock_storage.return_value = self.account_storage return _determine_account(self.args, self.config) @@ -131,7 +205,7 @@ class DetermineAccountTest(unittest.TestCase): self.assertEqual(self.accs[0].id, self.args.account) self.assertTrue(self.args.email is None) - @mock.patch("letsencrypt.client.display_ops.choose_account") + @mock.patch('letsencrypt.client.display_ops.choose_account') def test_multiple_accounts(self, mock_choose_accounts): for acc in self.accs: self.account_storage.save(acc) @@ -142,11 +216,11 @@ class DetermineAccountTest(unittest.TestCase): self.assertEqual(self.accs[1].id, self.args.account) self.assertTrue(self.args.email is None) - @mock.patch("letsencrypt.client.display_ops.get_email") + @mock.patch('letsencrypt.client.display_ops.get_email') def test_no_accounts_no_email(self, mock_get_email): - mock_get_email.return_value = "foo@bar.baz" + mock_get_email.return_value = 'foo@bar.baz' - with mock.patch("letsencrypt.cli.client") as client: + with mock.patch('letsencrypt.cli.client') as client: client.register.return_value = ( self.accs[0], mock.sentinel.acme) self.assertEqual((self.accs[0], mock.sentinel.acme), self._call()) @@ -154,15 +228,15 @@ class DetermineAccountTest(unittest.TestCase): self.config, self.account_storage, tos_cb=mock.ANY) self.assertEqual(self.accs[0].id, self.args.account) - self.assertEqual("foo@bar.baz", self.args.email) + self.assertEqual('foo@bar.baz', self.args.email) def test_no_accounts_email(self): - self.args.email = "other email" - with mock.patch("letsencrypt.cli.client") as client: + self.args.email = 'other email' + with mock.patch('letsencrypt.cli.client') as client: client.register.return_value = (self.accs[1], mock.sentinel.acme) self._call() self.assertEqual(self.accs[1].id, self.args.account) - self.assertEqual("other email", self.args.email) + self.assertEqual('other email', self.args.email) class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest): @@ -176,36 +250,36 @@ class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest): def tearDown(self): shutil.rmtree(self.tempdir) - @mock.patch("letsencrypt.le_util.make_or_verify_dir") + @mock.patch('letsencrypt.le_util.make_or_verify_dir') def test_find_duplicative_names(self, unused_makedir): from letsencrypt.cli import _find_duplicative_certs - test_cert = test_util.load_vector("cert-san.pem") - with open(self.test_rc.cert, "w") as f: + test_cert = test_util.load_vector('cert-san.pem') + with open(self.test_rc.cert, 'w') as f: f.write(test_cert) # No overlap at all - result = _find_duplicative_certs(["wow.net", "hooray.org"], + result = _find_duplicative_certs(['wow.net', 'hooray.org'], self.config, self.cli_config) self.assertEqual(result, (None, None)) # Totally identical - result = _find_duplicative_certs(["example.com", "www.example.com"], + result = _find_duplicative_certs(['example.com', 'www.example.com'], self.config, self.cli_config) - self.assertTrue(result[0].configfile.filename.endswith("example.org.conf")) + self.assertTrue(result[0].configfile.filename.endswith('example.org.conf')) self.assertEqual(result[1], None) # Superset - result = _find_duplicative_certs(["example.com", "www.example.com", - "something.new"], self.config, + result = _find_duplicative_certs(['example.com', 'www.example.com', + 'something.new'], self.config, self.cli_config) self.assertEqual(result[0], None) - self.assertTrue(result[1].configfile.filename.endswith("example.org.conf")) + self.assertTrue(result[1].configfile.filename.endswith('example.org.conf')) # Partial overlap doesn't count - result = _find_duplicative_certs(["example.com", "something.new"], + result = _find_duplicative_certs(['example.com', 'something.new'], self.config, self.cli_config) self.assertEqual(result, (None, None)) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() # pragma: no cover From dc0b26c2781132a1c3f0622c40c93f4e64bf1f53 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 28 Sep 2015 18:47:15 -0700 Subject: [PATCH 077/145] Raised cover percentage --- tox.cover.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.cover.sh b/tox.cover.sh index aa5e3ed88..edfd9b81a 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -16,7 +16,7 @@ fi cover () { if [ "$1" = "letsencrypt" ]; then - min=96 + min=97 elif [ "$1" = "acme" ]; then min=100 elif [ "$1" = "letsencrypt_apache" ]; then From ad1fce03f77feddcbf0ef96d1ff63ed40e44576f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 29 Sep 2015 06:47:15 +0000 Subject: [PATCH 078/145] UnrecognizedChallenge (fixes #855). Overrides quick fix from #856. --- acme/acme/challenges.py | 37 ++++++++++++++++++++++++++++++++----- acme/acme/messages.py | 14 +------------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index fbb2e7418..4731c043f 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -25,6 +25,14 @@ class Challenge(jose.TypedJSONObjectWithFields): """ACME challenge.""" TYPES = {} + @classmethod + def from_json(cls, jobj): + try: + return super(Challenge, cls).from_json(jobj) + except jose.UnrecognizedTypeError as error: + logger.debug(error) + return UnrecognizedChallenge.from_json(jobj) + class ContinuityChallenge(Challenge): # pylint: disable=abstract-method """Client validation challenges.""" @@ -34,11 +42,6 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method """Domain validation challenges.""" -class UnrecognizedChallenge(Challenge): - """Unrecognized challenge.""" - typ = "unknown" - - class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge response.""" @@ -47,6 +50,30 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields): resource = fields.Resource(resource_type) +class UnrecognizedChallenge(Challenge): + """Unrecognized challenge. + + ACME specification defines a generic framework for challenges and + defines some standard challenges that are implemented in this + module. However, other implementations (including peers) might + define additional challenge types, which should be ignored if + unrecognized. + + :ivar jobj: Original JSON decoded object. + + """ + + def __init__(self, jobj): + object.__setattr__(self, "jobj", jobj) + + def to_partial_json(self): + return self.jobj + + @classmethod + def from_json(cls, jobj): + return cls(jobj) + + @Challenge.register class SimpleHTTP(DVChallenge): """ACME "simpleHttp" challenge. diff --git a/acme/acme/messages.py b/acme/acme/messages.py index d6e9952c3..02ae24c8f 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -373,19 +373,7 @@ class Authorization(ResourceBody): @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument - # The from_json method raises errors.UnrecognizedTypeError when a - # challenge of unknown type is encountered. We want to ignore this - # case. This forces us to do an explicit iteration, since list - # comprehensions can't handle exceptions. - challs = [] - for chall in value: - try: - challs.append(ChallengeBody.from_json(chall)) - except jose.UnrecognizedTypeError: - challs.append(ChallengeBody( - uri="UNKNOWN", chall=challenges.UnrecognizedChallenge, - status=STATUS_UNKNOWN)) - return tuple(challs) + return tuple(ChallengeBody.from_json(chall) for chall in value) @property def resolved_combinations(self): From 0ffef20a20522cf060c8c75f84ad6ab9a77470d2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 29 Sep 2015 07:02:33 +0000 Subject: [PATCH 079/145] UnrecognizedChallenge: fix tests and lint. --- acme/acme/challenges.py | 2 ++ acme/acme/challenges_test.py | 26 ++++++++++++++++++++++++++ acme/acme/messages_test.py | 18 ------------------ 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 4731c043f..d81e77f83 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -64,9 +64,11 @@ class UnrecognizedChallenge(Challenge): """ def __init__(self, jobj): + super(UnrecognizedChallenge, self).__init__() object.__setattr__(self, "jobj", jobj) def to_partial_json(self): + # pylint: disable=no-member return self.jobj @classmethod diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index c82d95e19..ed44d4c45 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -17,6 +17,32 @@ CERT = test_util.load_cert('cert.pem') KEY = test_util.load_rsa_private_key('rsa512_key.pem') +class ChallengeTest(unittest.TestCase): + + def test_from_json_unrecognized(self): + from acme.challenges import Challenge + from acme.challenges import UnrecognizedChallenge + chall = UnrecognizedChallenge({"type": "foo"}) + # pylint: disable=no-member + self.assertEqual(chall, Challenge.from_json(chall.jobj)) + + +class UnrecognizedChallengeTest(unittest.TestCase): + + def setUp(self): + from acme.challenges import UnrecognizedChallenge + self.jobj = {"type": "foo"} + self.chall = UnrecognizedChallenge(self.jobj) + + def test_to_partial_json(self): + self.assertEqual(self.jobj, self.chall.to_partial_json()) + + def test_from_json(self): + from acme.challenges import UnrecognizedChallenge + self.assertEqual( + self.chall, UnrecognizedChallenge.from_json(self.jobj)) + + class SimpleHTTPTest(unittest.TestCase): def setUp(self): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index d7bbdb0e4..d2d859bc5 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -301,19 +301,6 @@ class AuthorizationTest(unittest.TestCase): 'combinations': combinations, } - # For unknown challenge types - self.jmsg_unknown_chall = { - 'resource': 'challenge', - 'uri': 'random_uri', - 'type': 'unknown', - 'tls': True, - } - - self.jobj_from_unknown = { - 'identifier': identifier.to_json(), - 'challenges': [self.jmsg_unknown_chall], - } - def test_from_json(self): from acme.messages import Authorization Authorization.from_json(self.jobj_from) @@ -328,11 +315,6 @@ class AuthorizationTest(unittest.TestCase): (self.challbs[1], self.challbs[2]), )) - def test_unknown_chall_type(self): - """Just make sure an error isn't thrown.""" - from acme.messages import Authorization - Authorization.from_json(self.jobj_from_unknown) - class AuthorizationResourceTest(unittest.TestCase): """Tests for acme.messages.AuthorizationResource.""" From dcd274ed93182caaf225e33d8efbb50666bb49fa Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 29 Sep 2015 11:06:02 -0700 Subject: [PATCH 080/145] Marked Nginx as Alpha --- letsencrypt-nginx/letsencrypt_nginx/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 2899e1f76..3f6d6f327 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -56,7 +56,7 @@ class NginxConfigurator(common.Plugin): zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) zope.interface.classProvides(interfaces.IPluginFactory) - description = "Nginx Web Server" + description = "Nginx Web Server - Alpha" @classmethod def add_parser_arguments(cls, add): From 312057b1b817254914256972dc326af3dbdece48 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 29 Sep 2015 12:54:52 -0700 Subject: [PATCH 081/145] changes += kuba_feedback --- letsencrypt/tests/cli_test.py | 13 ++++++++----- letsencrypt/tests/reporter_test.py | 6 ++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 31cef584b..a59bc414e 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -98,8 +98,7 @@ class CLITest(unittest.TestCase): mock_renewal.return_value = None with mock.patch('letsencrypt.cli._init_le_client') as mock_init: mock_init.return_value = mock_client - self._call(['-d', 'foo.bar', '-a', - 'standalone', '-i', 'bad', 'auth']) + self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth']) @mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli._treat_as_renewal') @@ -124,16 +123,20 @@ class CLITest(unittest.TestCase): self.assertTrue( cert_path in mock_get_utility().add_message.call_args[0][0]) + @mock.patch('letsencrypt.cli.display_ops.pick_installer') @mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli._init_le_client') - def test_auth_csr(self, mock_init, mock_get_utility): + def test_auth_csr(self, mock_init, mock_get_utility, mock_pick_installer): cert_path = '/etc/letsencrypt/live/foo.bar' mock_client = mock.MagicMock() mock_client.obtain_certificate_from_csr.return_value = ('certr', 'chain') mock_init.return_value = mock_client - self._call(['-a', 'standalone', 'auth', '--csr', CSR, - '--cert-path', cert_path, '--chain-path', '/']) + installer = 'installer' + self._call( + ['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR, + '--cert-path', cert_path, '--chain-path', '/']) + self.assertEqual(mock_pick_installer.call_args[0][1], installer) mock_client.save_certificate.assert_called_once_with( 'certr', 'chain', cert_path, '/') self.assertTrue( diff --git a/letsencrypt/tests/reporter_test.py b/letsencrypt/tests/reporter_test.py index ddf345c4c..c848b1cab 100644 --- a/letsencrypt/tests/reporter_test.py +++ b/letsencrypt/tests/reporter_test.py @@ -83,8 +83,10 @@ class ReporterTest(unittest.TestCase): def _add_messages(self): self.reporter.add_message("High", self.reporter.HIGH_PRIORITY) - self.reporter.add_message("Med", self.reporter.MEDIUM_PRIORITY, False) - self.reporter.add_message("Low", self.reporter.LOW_PRIORITY, False) + self.reporter.add_message( + "Med", self.reporter.MEDIUM_PRIORITY, on_crash=False) + self.reporter.add_message( + "Low", self.reporter.LOW_PRIORITY, on_crash=False) if __name__ == "__main__": From 2e0fd36c2831db4fcdaefdd5c43fac41ee7fbac6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 29 Sep 2015 21:01:02 +0000 Subject: [PATCH 082/145] Improve flag and help processing * letsencrypt --help $SUBCOMMAND now works. Fixes #787 #819 * subcommand arguments are now actually argument groups, so that all flags can be placed before or after subcommand verbs as the user wishes Fixes: #820 A limitation: * args like --cert-path were previously present for multiple verbs (auth/install/revoke) with separate docs; they are now in the "paths" topic. That's fine, though it would be good to *also* list them when the user types letsencrypt --help install. --- letsencrypt/cli.py | 64 ++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 53609009b..ac2c55551 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -489,8 +489,6 @@ class SilentParser(object): # pylint: disable=too-few-public-methods self.parser.add_argument(*args, **kwargs) -HELP_TOPICS = ["all", "security", "paths", "automation", "testing", "plugins"] - class HelpfulArgumentParser(object): """Argparse Wrapper. @@ -529,12 +527,17 @@ class HelpfulArgumentParser(object): def preprocess_args(self, args): """Work around some limitations in argparse. - Currently: add the default verb "run" as a default, and ensure that the + Currently: add the default verb "run" as a default, and ensure that the subcommand / verb comes last. """ + + if "-h" in args or "--help" in args: + # all verbs double as help arguments; don't get them confused + return args + for i,token in enumerate(args): if token in VERBS: - reordered = args[:i] + args[i+1:] + [args[i]] + reordered = args[:i] + args[i+1:] + [args[i]] return reordered return args + ["run"] @@ -717,6 +720,9 @@ def create_parser(plugins, args): VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", "plugins", "--help", "-h"] +HELP_TOPICS = (["all", "security", "paths", "automation", "testing", "apache", "nginx"] + + [v for v in VERBS if "-" not in v]) + def _create_subparsers(helpful): subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") @@ -732,53 +738,34 @@ def _create_subparsers(helpful): add_subparser("run", run) parser_auth = add_subparser("auth", auth) - helpful.add_group("auth", "Options for modifying how a cert is obtained") + helpful.add_group("auth", description="Options for modifying how a cert is obtained") parser_install = add_subparser("install", install) - helpful.add_group("install", "Options for modifying how a cert is deployed") + helpful.add_group("install", description="Options for modifying how a cert is deployed") parser_revoke = add_subparser("revoke", revoke) - helpful.add_group("revoke", "Options for revocation of certs") + helpful.add_group("revoke", description="Options for revocation of certs") parser_rollback = add_subparser("rollback", rollback) - helpful.add_group("rollback", "Options for reverting config changes") + helpful.add_group("rollback", description="Options for reverting config changes") add_subparser("config_changes", config_changes) parser_plugins = add_subparser("plugins", plugins_cmd) - helpful.add_group("plugins", "Plugin options") + helpful.add_group("plugins", description="Plugin options") - helpful.add("auth", - "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER format.") - helpful.add("auth", - "--cert-path", default=flag_default("auth_cert_path"), - help="When using --csr this is where certificate is saved.") helpful.add("auth", - "--chain-path", default=flag_default("auth_chain_path"), - help="When using --csr this is where certificate chain is saved.") - - helpful.add("install", - "--cert-path", required=True, help="Path to a certificate that is going to be installed.") - helpful.add("install", - "--key-path", required=True, help="Accompanying private key") - helpful.add("install", - "--chain-path", help="Accompanying path to a certificate chain.") - helpful.add("revoke", - "--cert-path", type=read_file, help="Revoke a specific certificate.", required=True) - helpful.add("revoke", - "--key-path", type=read_file, - help="Revoke certificate using its accompanying key. Useful if Account Key is lost.") - - helpful.add("rollback", + "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER format.") + helpful.add("rollback", "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), help="Revert configuration N number of checkpoints.") - helpful.add("plugins", + helpful.add("plugins", "--init", action="store_true", help="Initialize plugins.") - helpful.add("plugins", + helpful.add("plugins", "--prepare", action="store_true", help="Initialize and prepare plugins.") - helpful.add("plugins", + helpful.add("plugins", "--authenticators", action="append_const", dest="ifaces", const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.") - helpful.add("plugins", + helpful.add("plugins", "--installers", action="append_const", dest="ifaces", const=interfaces.IInstaller, help="Limit to installer plugins only.") @@ -787,6 +774,15 @@ def _paths_parser(helpful): add = helpful.add helpful.add_group( "paths", description="Arguments changing execution paths & servers") + helpful.add("paths", + "--cert-path", default=flag_default("auth_cert_path"), + help="Path to where certificate is saved (with auth), " + "installed (with install --csr) or revoked.") + helpful.add("paths", + "--key-path", required=True, + help="Path to private key for cert creation or revocation (if account key is missing)") + helpful.add("paths", + "--chain-path", help="Accompanying path to a certificate chain.") add("paths", "--config-dir", default=flag_default("config_dir"), help=config_help("config_dir")) add("paths", "--work-dir", default=flag_default("work_dir"), From a0af023b1436e27f5c1a7626aeeab374d927cf3b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 29 Sep 2015 14:48:26 -0700 Subject: [PATCH 083/145] --key-path is mandatory for install, optional for revoke --- letsencrypt/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index ac2c55551..8042173e8 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -530,7 +530,6 @@ class HelpfulArgumentParser(object): Currently: add the default verb "run" as a default, and ensure that the subcommand / verb comes last. """ - if "-h" in args or "--help" in args: # all verbs double as help arguments; don't get them confused return args @@ -779,7 +778,7 @@ def _paths_parser(helpful): help="Path to where certificate is saved (with auth), " "installed (with install --csr) or revoked.") helpful.add("paths", - "--key-path", required=True, + "--key-path", required=("install" in helpful.args), help="Path to private key for cert creation or revocation (if account key is missing)") helpful.add("paths", "--chain-path", help="Accompanying path to a certificate chain.") From 05d439a33937c4c46d2ee949dc70c9126f463efd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 29 Sep 2015 14:48:40 -0700 Subject: [PATCH 084/145] Update cli tests We don't expect to error out if called with no args --- letsencrypt/tests/cli_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 2e9f3330c..992b254a7 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -40,7 +40,9 @@ class CLITest(unittest.TestCase): return ret, stdout, stderr, client def test_no_flags(self): - self.assertRaises(SystemExit, self._call, []) + with mock.patch('letsencrypt.cli.run') as mock_run: + self._call([]) + self.assertEqual(1, mock_run.call_count) def test_help(self): self.assertRaises(SystemExit, self._call, ['--help']) From 2297349b95f1451d10caae24297ba3b84dd7d6ce Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 29 Sep 2015 16:56:36 -0700 Subject: [PATCH 085/145] lintian --- letsencrypt/cli.py | 24 ++++++++++++------------ letsencrypt/tests/cli_test.py | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 0d72a3eb5..b7efa041a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -500,7 +500,6 @@ class SilentParser(object): # pylint: disable=too-few-public-methods self.parser.add_argument(*args, **kwargs) - class HelpfulArgumentParser(object): """Argparse Wrapper. @@ -545,7 +544,7 @@ class HelpfulArgumentParser(object): # all verbs double as help arguments; don't get them confused return args - for i,token in enumerate(args): + for i, token in enumerate(args): if token in VERBS: reordered = args[:i] + args[i+1:] + [args[i]] return reordered @@ -745,18 +744,21 @@ def _create_subparsers(helpful): # the order of add_subparser() calls is important: it defines the # order in which subparser names will be displayed in --help - add_subparser("run", run) + # these add_subparser objects return objects to which arguments could be + # attached, but they have annoying arg ordering constrains so we use + # groups instead: https://github.com/letsencrypt/letsencrypt/issues/820 - parser_auth = add_subparser("auth", auth) + add_subparser("run", run) + add_subparser("auth", auth) helpful.add_group("auth", description="Options for modifying how a cert is obtained") - parser_install = add_subparser("install", install) + add_subparser("install", install) helpful.add_group("install", description="Options for modifying how a cert is deployed") - parser_revoke = add_subparser("revoke", revoke) + add_subparser("revoke", revoke) helpful.add_group("revoke", description="Options for revocation of certs") - parser_rollback = add_subparser("rollback", rollback) + add_subparser("rollback", rollback) helpful.add_group("rollback", description="Options for reverting config changes") add_subparser("config_changes", config_changes) - parser_plugins = add_subparser("plugins", plugins_cmd) + add_subparser("plugins", plugins_cmd) helpful.add_group("plugins", description="Plugin options") helpful.add("auth", @@ -769,12 +771,10 @@ def _create_subparsers(helpful): helpful.add("plugins", "--init", action="store_true", help="Initialize plugins.") helpful.add("plugins", - "--prepare", action="store_true", - help="Initialize and prepare plugins.") + "--prepare", action="store_true", help="Initialize and prepare plugins.") helpful.add("plugins", "--authenticators", action="append_const", dest="ifaces", - const=interfaces.IAuthenticator, - help="Limit to authenticator plugins only.") + const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.") helpful.add("plugins", "--installers", action="append_const", dest="ifaces", const=interfaces.IInstaller, help="Limit to installer plugins only.") diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index ce32b8f78..9a99a74cc 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -44,8 +44,8 @@ class CLITest(unittest.TestCase): def test_no_flags(self): with mock.patch('letsencrypt.cli.run') as mock_run: - self._call([]) - self.assertEqual(1, mock_run.call_count) + self._call([]) + self.assertEqual(1, mock_run.call_count) def test_help(self): self.assertRaises(SystemExit, self._call, ['--help']) From 6b6bc038827e359173039a5cb229104ef257e127 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 29 Sep 2015 17:12:38 -0700 Subject: [PATCH 086/145] --cert-path was required for install and revoke Oops --- letsencrypt/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index b7efa041a..82bd57ec8 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -786,6 +786,7 @@ def _paths_parser(helpful): "paths", description="Arguments changing execution paths & servers") helpful.add("paths", "--cert-path", default=flag_default("auth_cert_path"), + required=("install" in helpful.args or "revoke" in helpful.args), help="Path to where certificate is saved (with auth), " "installed (with install --csr) or revoked.") helpful.add("paths", From 18dacc528df67e703336bd778518a25f4850b345 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 29 Sep 2015 18:08:58 -0700 Subject: [PATCH 087/145] Preserve all argparse parameters Try to restore all variants that applied to the different subcomannds --- letsencrypt/cli.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 82bd57ec8..ac79ab93c 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -521,6 +521,7 @@ class HelpfulArgumentParser(object): self.parser._add_config_file_help = False # pylint: disable=protected-access self.silent_parser = SilentParser(self.parser) + self.verb = None self.args = self.preprocess_args(args) help1 = self.prescan_for_flag("-h", self.help_topics) help2 = self.prescan_for_flag("--help", self.help_topics) @@ -542,12 +543,16 @@ class HelpfulArgumentParser(object): """ if "-h" in args or "--help" in args: # all verbs double as help arguments; don't get them confused + self.verb = "help" return args for i, token in enumerate(args): if token in VERBS: reordered = args[:i] + args[i+1:] + [args[i]] + self.verb = token return reordered + + self.verb = "run" return args + ["run"] def prescan_for_flag(self, flag, possible_arguments): @@ -782,18 +787,28 @@ def _create_subparsers(helpful): def _paths_parser(helpful): add = helpful.add + verb = helpful.verb helpful.add_group( "paths", description="Arguments changing execution paths & servers") - helpful.add("paths", - "--cert-path", default=flag_default("auth_cert_path"), - required=("install" in helpful.args or "revoke" in helpful.args), - help="Path to where certificate is saved (with auth), " - "installed (with install --csr) or revoked.") - helpful.add("paths", - "--key-path", required=("install" in helpful.args), + + cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked." + if verb == "auth": + add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph) + elif verb == "revoke": + add("paths", "--cert-path", type=read_file, required=True, help=cph) + else: + add("paths", "--cert-path", help=cph, required=(verb == "install")) + + # revoke --key-path reads a file, install --key-path takes a string + add("paths", "--key-path", type=((verb == "revoke" and read_file) or str), + required=(verb == "install"), help="Path to private key for cert creation or revocation (if account key is missing)") - helpful.add("paths", - "--chain-path", help="Accompanying path to a certificate chain.") + + default_cp = None + if verb == "auth": + default_cp = flag_default("auth_chain_path") + add("paths", "--chain-path", default=default_cp, + help="Accompanying path to a certificate chain.") add("paths", "--config-dir", default=flag_default("config_dir"), help=config_help("config_dir")) add("paths", "--work-dir", default=flag_default("work_dir"), From 627fca37b4e45d300fdb3a023943b11c6bbae593 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 29 Sep 2015 18:18:18 -0700 Subject: [PATCH 088/145] We didn't actually need to define --help as a verb --- letsencrypt/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index ac79ab93c..efc0f9f70 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -732,10 +732,10 @@ def create_parser(plugins, args): # For now unfortunately this constant just needs to match the code below; # there isn't an elegant way to autogenerate it in time. VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", - "plugins", "--help", "-h"] + "plugins"] HELP_TOPICS = (["all", "security", "paths", "automation", "testing", "apache", "nginx"] + - [v for v in VERBS if "-" not in v]) + [v for v in VERBS]) def _create_subparsers(helpful): From 1e3c92c714bb382298343d5c14f14aa896e765ab Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 11:49:46 -0700 Subject: [PATCH 089/145] Cleanup the verb -> subparser mapping --- letsencrypt/cli.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index efc0f9f70..fd9d8cbb6 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -9,6 +9,7 @@ import os import pkg_resources import sys import time +import types import traceback import configargparse @@ -731,17 +732,16 @@ def create_parser(plugins, args): # For now unfortunately this constant just needs to match the code below; # there isn't an elegant way to autogenerate it in time. -VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", - "plugins"] - -HELP_TOPICS = (["all", "security", "paths", "automation", "testing", "apache", "nginx"] + - [v for v in VERBS]) - +VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", "plugins"] +HELP_TOPICS = (["all", "security", "paths", "automation", "testing", "apache", "nginx"] + VERBS) def _create_subparsers(helpful): subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") - def add_subparser(name, func): # pylint: disable=missing-docstring + def add_subparser(name): # pylint: disable=missing-docstring + # Each subcommand is implemented by a function of the same name + func = eval(name) # pylint: disable=eval-used + assert isinstance(func, types.FunctionType), "squirrels in namespace" subparser = subparsers.add_parser( name, help=func.__doc__.splitlines()[0], description=func.__doc__) subparser.set_defaults(func=func) @@ -752,18 +752,13 @@ def _create_subparsers(helpful): # these add_subparser objects return objects to which arguments could be # attached, but they have annoying arg ordering constrains so we use # groups instead: https://github.com/letsencrypt/letsencrypt/issues/820 + for v in VERBS: + add_subparser(v) - add_subparser("run", run) - add_subparser("auth", auth) helpful.add_group("auth", description="Options for modifying how a cert is obtained") - add_subparser("install", install) helpful.add_group("install", description="Options for modifying how a cert is deployed") - add_subparser("revoke", revoke) helpful.add_group("revoke", description="Options for revocation of certs") - add_subparser("rollback", rollback) helpful.add_group("rollback", description="Options for reverting config changes") - add_subparser("config_changes", config_changes) - add_subparser("plugins", plugins_cmd) helpful.add_group("plugins", description="Plugin options") helpful.add("auth", From 2a3a111d628711e131ea202511a1383c10dfe378 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 12:09:38 -0700 Subject: [PATCH 090/145] Disable pylint invalid-name It's clearly making our code harder to read and write --- .pylintrc | 2 +- acme/acme/challenges.py | 4 ++-- acme/acme/challenges_test.py | 2 +- acme/acme/jose/interfaces_test.py | 4 ++-- acme/acme/jose/json_util_test.py | 6 +++--- acme/acme/jose/jwa_test.py | 2 +- acme/acme/jose/jwk.py | 2 +- acme/acme/jose/util.py | 4 ++-- acme/acme/jose/util_test.py | 4 ++-- acme/acme/messages_test.py | 2 +- letsencrypt/account.py | 2 +- letsencrypt/display/enhancements.py | 2 +- letsencrypt/display/ops.py | 2 +- letsencrypt/log.py | 2 +- letsencrypt/plugins/common.py | 6 +++--- letsencrypt/tests/auth_handler_test.py | 2 +- letsencrypt/tests/log_test.py | 2 +- letsencrypt/tests/reverter_test.py | 8 ++++---- 18 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.pylintrc b/.pylintrc index bf318704a..268d61ec6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -38,7 +38,7 @@ load-plugins=linter_plugin # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use +disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name # abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index d81e77f83..f5763adc4 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -315,7 +315,7 @@ class DVSNIResponse(ChallengeResponse): validation = jose.Field("validation", decoder=jose.JWS.from_json) @property - def z(self): # pylint: disable=invalid-name + def z(self): """The ``z`` parameter. :rtype: bytes @@ -333,7 +333,7 @@ class DVSNIResponse(ChallengeResponse): :rtype: bytes """ - z = self.z # pylint: disable=invalid-name + z = self.z return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX @property diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index ed44d4c45..b3f48cdf2 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -269,7 +269,7 @@ class DVSNIResponseTest(unittest.TestCase): 'validation': self.validation.to_json(), } - # pylint: disable=invalid-name + label1 = b'e2df3498860637c667fedadc5a8494ec' label2 = b'09dcc75553c9b3bd73662b50e71b1e42' self.z = label1 + label2 diff --git a/acme/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py index 380c3a2a5..a3ee124ff 100644 --- a/acme/acme/jose/interfaces_test.py +++ b/acme/acme/jose/interfaces_test.py @@ -8,7 +8,7 @@ class JSONDeSerializableTest(unittest.TestCase): def setUp(self): from acme.jose.interfaces import JSONDeSerializable - # pylint: disable=missing-docstring,invalid-name + # pylint: disable=missing-docstring class Basic(JSONDeSerializable): def __init__(self, v): @@ -53,7 +53,7 @@ class JSONDeSerializableTest(unittest.TestCase): self.nested = Basic([[self.basic1]]) self.tuple = Basic(('foo',)) - # pylint: disable=invalid-name + self.Basic = Basic self.Sequence = Sequence self.Mapping = Mapping diff --git a/acme/acme/jose/json_util_test.py b/acme/acme/jose/json_util_test.py index a055f3bf7..f751382e0 100644 --- a/acme/acme/jose/json_util_test.py +++ b/acme/acme/jose/json_util_test.py @@ -92,7 +92,7 @@ class JSONObjectWithFieldsMetaTest(unittest.TestCase): from acme.jose.json_util import JSONObjectWithFieldsMeta self.field = Field('Baz') self.field2 = Field('Baz2') - # pylint: disable=invalid-name,missing-docstring,too-few-public-methods + # pylint: disable=missing-docstring,too-few-public-methods # pylint: disable=blacklisted-name @six.add_metaclass(JSONObjectWithFieldsMeta) @@ -138,7 +138,7 @@ class JSONObjectWithFieldsTest(unittest.TestCase): from acme.jose.json_util import Field class MockJSONObjectWithFields(JSONObjectWithFields): - # pylint: disable=invalid-name,missing-docstring,no-self-argument + # pylint: disable=missing-docstring,no-self-argument # pylint: disable=too-few-public-methods x = Field('x', omitempty=True, encoder=(lambda x: x * 2), @@ -158,7 +158,7 @@ class JSONObjectWithFieldsTest(unittest.TestCase): raise errors.DeserializationError() return value - # pylint: disable=invalid-name + self.MockJSONObjectWithFields = MockJSONObjectWithFields self.mock = MockJSONObjectWithFields(x=None, y=2, z=3) diff --git a/acme/acme/jose/jwa_test.py b/acme/acme/jose/jwa_test.py index 3328d083a..8ca512043 100644 --- a/acme/acme/jose/jwa_test.py +++ b/acme/acme/jose/jwa_test.py @@ -26,7 +26,7 @@ class JWASignatureTest(unittest.TestCase): def verify(self, key, msg, sig): raise NotImplementedError() # pragma: no cover - # pylint: disable=invalid-name + self.Sig1 = MockSig('Sig1') self.Sig2 = MockSig('Sig2') diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 7a976f189..67f243347 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -186,7 +186,7 @@ class JWKRSA(JWK): @classmethod def fields_from_json(cls, jobj): - # pylint: disable=invalid-name + n, e = (cls._decode_param(jobj[x]) for x in ('n', 'e')) public_numbers = rsa.RSAPublicNumbers(e=e, n=n) if 'd' not in jobj: # public key diff --git a/acme/acme/jose/util.py b/acme/acme/jose/util.py index ab3606efc..46c43bf35 100644 --- a/acme/acme/jose/util.py +++ b/acme/acme/jose/util.py @@ -7,7 +7,7 @@ import six class abstractclassmethod(classmethod): - # pylint: disable=invalid-name,too-few-public-methods + # pylint: disable=too-few-public-methods """Descriptor for an abstract classmethod. It augments the :mod:`abc` framework with an abstract @@ -172,7 +172,7 @@ class ImmutableMap(collections.Mapping, collections.Hashable): class frozendict(collections.Mapping, collections.Hashable): - # pylint: disable=invalid-name,too-few-public-methods + # pylint: disable=too-few-public-methods """Frozen dictionary.""" __slots__ = ('_items', '_keys') diff --git a/acme/acme/jose/util_test.py b/acme/acme/jose/util_test.py index 4cdd9127f..295c70fee 100644 --- a/acme/acme/jose/util_test.py +++ b/acme/acme/jose/util_test.py @@ -92,7 +92,7 @@ class ImmutableMapTest(unittest.TestCase): """Tests for acme.jose.util.ImmutableMap.""" def setUp(self): - # pylint: disable=invalid-name,too-few-public-methods + # pylint: disable=too-few-public-methods # pylint: disable=missing-docstring from acme.jose.util import ImmutableMap @@ -156,7 +156,7 @@ class ImmutableMapTest(unittest.TestCase): self.assertEqual("B(x='foo', y='bar')", repr(self.B(x='foo', y='bar'))) -class frozendictTest(unittest.TestCase): # pylint: disable=invalid-name +class frozendictTest(unittest.TestCase): """Tests for acme.jose.util.frozendict.""" def setUp(self): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index d2d859bc5..718a936dd 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -64,7 +64,7 @@ class ConstantTest(unittest.TestCase): class MockConstant(_Constant): # pylint: disable=missing-docstring POSSIBLE_NAMES = {} - self.MockConstant = MockConstant # pylint: disable=invalid-name + self.MockConstant = MockConstant self.const_a = MockConstant('a') self.const_b = MockConstant('b') diff --git a/letsencrypt/account.py b/letsencrypt/account.py index c97e4f6fe..81d31b831 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -54,7 +54,7 @@ class Account(object): # pylint: disable=too-few-public-methods tz=pytz.UTC).replace(microsecond=0), creation_host=socket.getfqdn()) if meta is None else meta - self.id = hashlib.md5( # pylint: disable=invalid-name + self.id = hashlib.md5( self.key.key.public_key().public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo) diff --git a/letsencrypt/display/enhancements.py b/letsencrypt/display/enhancements.py index 8edc72ba0..c56198161 100644 --- a/letsencrypt/display/enhancements.py +++ b/letsencrypt/display/enhancements.py @@ -11,7 +11,7 @@ from letsencrypt.display import util as display_util logger = logging.getLogger(__name__) # Define a helper function to avoid verbose code -util = zope.component.getUtility # pylint: disable=invalid-name +util = zope.component.getUtility def ask(enhancement): diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 43705e309..cb424a81b 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -12,7 +12,7 @@ from letsencrypt.display import util as display_util logger = logging.getLogger(__name__) # Define a helper function to avoid verbose code -util = zope.component.getUtility # pylint: disable=invalid-name +util = zope.component.getUtility def choose_plugin(prepared, question): diff --git a/letsencrypt/log.py b/letsencrypt/log.py index e800d37c9..6436f6fc2 100644 --- a/letsencrypt/log.py +++ b/letsencrypt/log.py @@ -25,7 +25,7 @@ class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods logging.Handler.__init__(self, level) self.height = height self.width = width - # "dialog" collides with module name... pylint: disable=invalid-name + # "dialog" collides with module name... self.d = dialog.Dialog() if d is None else d self.lines = [] diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 59598a35e..95ad56a0a 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -23,10 +23,10 @@ def dest_namespace(name): """ArgumentParser dest namespace (prefix of all destinations).""" return name.replace("-", "_") + "_" -private_ips_regex = re.compile( # pylint: disable=invalid-name +private_ips_regex = re.compile( r"(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|" r"(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)") -hostname_regex = re.compile( # pylint: disable=invalid-name +hostname_regex = re.compile( r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$", re.IGNORECASE) @@ -173,7 +173,7 @@ class Dvsni(object): achall.chall.encode("token") + '.pem') def _setup_challenge_cert(self, achall, s=None): - # pylint: disable=invalid-name + """Generate and write out challenge certificate.""" cert_path = self.get_cert_path(achall) key_path = self.get_key_path(achall) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index ed29ead25..18ee56081 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -355,7 +355,7 @@ class GenChallengePathTest(unittest.TestCase): class MutuallyExclusiveTest(unittest.TestCase): """Tests for letsencrypt.auth_handler.mutually_exclusive.""" - # pylint: disable=invalid-name,missing-docstring,too-few-public-methods + # pylint: disable=missing-docstring,too-few-public-methods class A(object): pass diff --git a/letsencrypt/tests/log_test.py b/letsencrypt/tests/log_test.py index 50d0712e7..c1afd2c8a 100644 --- a/letsencrypt/tests/log_test.py +++ b/letsencrypt/tests/log_test.py @@ -8,7 +8,7 @@ import mock class DialogHandlerTest(unittest.TestCase): def setUp(self): - self.d = mock.MagicMock() # pylint: disable=invalid-name + self.d = mock.MagicMock() from letsencrypt.log import DialogHandler self.handler = DialogHandler(height=2, width=6, d=self.d) diff --git a/letsencrypt/tests/reverter_test.py b/letsencrypt/tests/reverter_test.py index 62c47f8d6..d31b6f2cc 100644 --- a/letsencrypt/tests/reverter_test.py +++ b/letsencrypt/tests/reverter_test.py @@ -85,7 +85,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.assertEqual(read_in(self.config1), "directive-dir1") def test_multiple_registration_fail_and_revert(self): - # pylint: disable=invalid-name + config3 = os.path.join(self.dir1, "config3.txt") update_file(config3, "Config3") config4 = os.path.join(self.dir2, "config4.txt") @@ -173,7 +173,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.assertRaises(errors.ReverterError, self.reverter.recovery_routine) def test_recover_checkpoint_revert_temp_failures(self): - # pylint: disable=invalid-name + mock_recover = mock.MagicMock( side_effect=errors.ReverterError("e")) @@ -291,7 +291,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): errors.ReverterError, self.reverter.rollback_checkpoints, "one") def test_rollback_finalize_checkpoint_valid_inputs(self): - # pylint: disable=invalid-name + config3 = self._setup_three_checkpoints() # Check resulting backup directory @@ -334,7 +334,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): @mock.patch("letsencrypt.reverter.os.rename") def test_finalize_checkpoint_no_rename_directory(self, mock_rename): - # pylint: disable=invalid-name + self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_rename.side_effect = OSError From 2d578468bde4cbba2138216633de44bfdd46cf04 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 12:32:44 -0700 Subject: [PATCH 091/145] Use a verb -> function table instead of eval() - plugins_cmd() not plugins() broke the more minimalist eval() approach - more wrangling was required to mock out calls via the VERBS table --- letsencrypt/cli.py | 25 +++++++++++++++---------- letsencrypt/tests/cli_test.py | 2 ++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index fd9d8cbb6..66f991063 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -9,7 +9,6 @@ import os import pkg_resources import sys import time -import types import traceback import configargparse @@ -731,17 +730,23 @@ def create_parser(plugins, args): return helpful.parser, helpful.args # For now unfortunately this constant just needs to match the code below; -# there isn't an elegant way to autogenerate it in time. -VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", "plugins"] -HELP_TOPICS = (["all", "security", "paths", "automation", "testing", "apache", "nginx"] + VERBS) +# there isn't an elegant way to autogenerate it in time. pylint: disable=bad-whitespace +VERBS = { + "run" : run, + "auth" : auth, + "install" : install, + "revoke" : revoke, + "rollback" : rollback, + "config_changes" : config_changes, + "plugins" : plugins_cmd +} +HELP_TOPICS = (["all", "security", "paths", "automation", "testing", "apache", "nginx"] + + VERBS.keys()) def _create_subparsers(helpful): subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") - def add_subparser(name): # pylint: disable=missing-docstring - # Each subcommand is implemented by a function of the same name - func = eval(name) # pylint: disable=eval-used - assert isinstance(func, types.FunctionType), "squirrels in namespace" + def add_subparser(name, func): # pylint: disable=missing-docstring subparser = subparsers.add_parser( name, help=func.__doc__.splitlines()[0], description=func.__doc__) subparser.set_defaults(func=func) @@ -752,8 +757,8 @@ def _create_subparsers(helpful): # these add_subparser objects return objects to which arguments could be # attached, but they have annoying arg ordering constrains so we use # groups instead: https://github.com/letsencrypt/letsencrypt/issues/820 - for v in VERBS: - add_subparser(v) + for v, func in VERBS.items(): + add_subparser(v, func) helpful.add_group("auth", description="Options for modifying how a cert is obtained") helpful.add_group("install", description="Options for modifying how a cert is deployed") diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 9a99a74cc..f5613ee58 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -44,6 +44,8 @@ class CLITest(unittest.TestCase): def test_no_flags(self): with mock.patch('letsencrypt.cli.run') as mock_run: + from letsencrypt import cli + cli.VERBS["run"] = mock_run self._call([]) self.assertEqual(1, mock_run.call_count) From bb167743f32f6b1d84a25295505d255aea331d5c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 30 Sep 2015 13:00:10 -0700 Subject: [PATCH 092/145] Don't call_registered() on SystemExit --- letsencrypt/error_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py index 1f979a6de..1292f2bc5 100644 --- a/letsencrypt/error_handler.py +++ b/letsencrypt/error_handler.py @@ -50,7 +50,7 @@ class ErrorHandler(object): self.set_signal_handlers() def __exit__(self, exec_type, exec_value, trace): - if exec_value is not None: + if exec_type not in (None, SystemExit): logger.debug("Encountered exception:\n%s", "".join( traceback.format_exception(exec_type, exec_value, trace))) self.call_registered() From d85f42d71f5aba91cb96af6f9959c778fb047b9f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 15:29:29 -0700 Subject: [PATCH 093/145] Plugins don't need to be in HELP_TOPICS They're already added as topics automatically, though they do need to be in the hand-written top level help. --- letsencrypt/cli.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 66f991063..1ad57b738 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -80,8 +80,8 @@ More detailed help: -h, --help [topic] print this message, or detailed help on a topic; the available topics are: - all, apache, automation, nginx, paths, security, testing, or any of the - subcommands + all, apache, automation, manual, nginx, paths, security, testing, or any of + the subcommands """ @@ -740,8 +740,7 @@ VERBS = { "config_changes" : config_changes, "plugins" : plugins_cmd } -HELP_TOPICS = (["all", "security", "paths", "automation", "testing", "apache", "nginx"] - + VERBS.keys()) +HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + VERBS.keys() def _create_subparsers(helpful): subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") From 5ca1a27200fb17ac04104ba65c05d810bb20b906 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 15:31:32 -0700 Subject: [PATCH 094/145] Keep the acme/ subtree compatible with strict pylinting --- acme/acme/challenges.py | 4 ++-- acme/acme/challenges_test.py | 2 +- acme/acme/jose/interfaces_test.py | 4 ++-- acme/acme/jose/json_util_test.py | 6 +++--- acme/acme/jose/jwa_test.py | 2 +- acme/acme/jose/jwk.py | 2 +- acme/acme/jose/util.py | 4 ++-- acme/acme/jose/util_test.py | 4 ++-- acme/acme/messages_test.py | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index f5763adc4..d81e77f83 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -315,7 +315,7 @@ class DVSNIResponse(ChallengeResponse): validation = jose.Field("validation", decoder=jose.JWS.from_json) @property - def z(self): + def z(self): # pylint: disable=invalid-name """The ``z`` parameter. :rtype: bytes @@ -333,7 +333,7 @@ class DVSNIResponse(ChallengeResponse): :rtype: bytes """ - z = self.z + z = self.z # pylint: disable=invalid-name return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX @property diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index b3f48cdf2..ed44d4c45 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -269,7 +269,7 @@ class DVSNIResponseTest(unittest.TestCase): 'validation': self.validation.to_json(), } - + # pylint: disable=invalid-name label1 = b'e2df3498860637c667fedadc5a8494ec' label2 = b'09dcc75553c9b3bd73662b50e71b1e42' self.z = label1 + label2 diff --git a/acme/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py index a3ee124ff..380c3a2a5 100644 --- a/acme/acme/jose/interfaces_test.py +++ b/acme/acme/jose/interfaces_test.py @@ -8,7 +8,7 @@ class JSONDeSerializableTest(unittest.TestCase): def setUp(self): from acme.jose.interfaces import JSONDeSerializable - # pylint: disable=missing-docstring + # pylint: disable=missing-docstring,invalid-name class Basic(JSONDeSerializable): def __init__(self, v): @@ -53,7 +53,7 @@ class JSONDeSerializableTest(unittest.TestCase): self.nested = Basic([[self.basic1]]) self.tuple = Basic(('foo',)) - + # pylint: disable=invalid-name self.Basic = Basic self.Sequence = Sequence self.Mapping = Mapping diff --git a/acme/acme/jose/json_util_test.py b/acme/acme/jose/json_util_test.py index f751382e0..a055f3bf7 100644 --- a/acme/acme/jose/json_util_test.py +++ b/acme/acme/jose/json_util_test.py @@ -92,7 +92,7 @@ class JSONObjectWithFieldsMetaTest(unittest.TestCase): from acme.jose.json_util import JSONObjectWithFieldsMeta self.field = Field('Baz') self.field2 = Field('Baz2') - # pylint: disable=missing-docstring,too-few-public-methods + # pylint: disable=invalid-name,missing-docstring,too-few-public-methods # pylint: disable=blacklisted-name @six.add_metaclass(JSONObjectWithFieldsMeta) @@ -138,7 +138,7 @@ class JSONObjectWithFieldsTest(unittest.TestCase): from acme.jose.json_util import Field class MockJSONObjectWithFields(JSONObjectWithFields): - # pylint: disable=missing-docstring,no-self-argument + # pylint: disable=invalid-name,missing-docstring,no-self-argument # pylint: disable=too-few-public-methods x = Field('x', omitempty=True, encoder=(lambda x: x * 2), @@ -158,7 +158,7 @@ class JSONObjectWithFieldsTest(unittest.TestCase): raise errors.DeserializationError() return value - + # pylint: disable=invalid-name self.MockJSONObjectWithFields = MockJSONObjectWithFields self.mock = MockJSONObjectWithFields(x=None, y=2, z=3) diff --git a/acme/acme/jose/jwa_test.py b/acme/acme/jose/jwa_test.py index 8ca512043..3328d083a 100644 --- a/acme/acme/jose/jwa_test.py +++ b/acme/acme/jose/jwa_test.py @@ -26,7 +26,7 @@ class JWASignatureTest(unittest.TestCase): def verify(self, key, msg, sig): raise NotImplementedError() # pragma: no cover - + # pylint: disable=invalid-name self.Sig1 = MockSig('Sig1') self.Sig2 = MockSig('Sig2') diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 67f243347..7a976f189 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -186,7 +186,7 @@ class JWKRSA(JWK): @classmethod def fields_from_json(cls, jobj): - + # pylint: disable=invalid-name n, e = (cls._decode_param(jobj[x]) for x in ('n', 'e')) public_numbers = rsa.RSAPublicNumbers(e=e, n=n) if 'd' not in jobj: # public key diff --git a/acme/acme/jose/util.py b/acme/acme/jose/util.py index 46c43bf35..ab3606efc 100644 --- a/acme/acme/jose/util.py +++ b/acme/acme/jose/util.py @@ -7,7 +7,7 @@ import six class abstractclassmethod(classmethod): - # pylint: disable=too-few-public-methods + # pylint: disable=invalid-name,too-few-public-methods """Descriptor for an abstract classmethod. It augments the :mod:`abc` framework with an abstract @@ -172,7 +172,7 @@ class ImmutableMap(collections.Mapping, collections.Hashable): class frozendict(collections.Mapping, collections.Hashable): - # pylint: disable=too-few-public-methods + # pylint: disable=invalid-name,too-few-public-methods """Frozen dictionary.""" __slots__ = ('_items', '_keys') diff --git a/acme/acme/jose/util_test.py b/acme/acme/jose/util_test.py index 295c70fee..4cdd9127f 100644 --- a/acme/acme/jose/util_test.py +++ b/acme/acme/jose/util_test.py @@ -92,7 +92,7 @@ class ImmutableMapTest(unittest.TestCase): """Tests for acme.jose.util.ImmutableMap.""" def setUp(self): - # pylint: disable=too-few-public-methods + # pylint: disable=invalid-name,too-few-public-methods # pylint: disable=missing-docstring from acme.jose.util import ImmutableMap @@ -156,7 +156,7 @@ class ImmutableMapTest(unittest.TestCase): self.assertEqual("B(x='foo', y='bar')", repr(self.B(x='foo', y='bar'))) -class frozendictTest(unittest.TestCase): +class frozendictTest(unittest.TestCase): # pylint: disable=invalid-name """Tests for acme.jose.util.frozendict.""" def setUp(self): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index 718a936dd..d2d859bc5 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -64,7 +64,7 @@ class ConstantTest(unittest.TestCase): class MockConstant(_Constant): # pylint: disable=missing-docstring POSSIBLE_NAMES = {} - self.MockConstant = MockConstant + self.MockConstant = MockConstant # pylint: disable=invalid-name self.const_a = MockConstant('a') self.const_b = MockConstant('b') From 2406fc0486d8f74ad9979b1973e9e24d9d453df7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 15:56:58 -0700 Subject: [PATCH 095/145] Go back to VERBS as a list The dictionary was destroying the ordering, which was important. --- letsencrypt/cli.py | 28 ++++++++++++---------------- letsencrypt/tests/cli_test.py | 2 -- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 1ad57b738..73dd24bdb 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -730,24 +730,20 @@ def create_parser(plugins, args): return helpful.parser, helpful.args # For now unfortunately this constant just needs to match the code below; -# there isn't an elegant way to autogenerate it in time. pylint: disable=bad-whitespace -VERBS = { - "run" : run, - "auth" : auth, - "install" : install, - "revoke" : revoke, - "rollback" : rollback, - "config_changes" : config_changes, - "plugins" : plugins_cmd -} -HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + VERBS.keys() +# there isn't an elegant way to autogenerate it in time. +VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", "plugins"] +HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + VERBS def _create_subparsers(helpful): subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") - def add_subparser(name, func): # pylint: disable=missing-docstring - subparser = subparsers.add_parser( - name, help=func.__doc__.splitlines()[0], description=func.__doc__) + def add_subparser(name): # pylint: disable=missing-docstring + if name == "plugins": + func = plugins_cmd + else: + func = eval(name) # pylint: disable=eval-used + h = func.__doc__.splitlines()[0] + subparser = subparsers.add_parser(name, help=h, description=func.__doc__) subparser.set_defaults(func=func) return subparser @@ -756,8 +752,8 @@ def _create_subparsers(helpful): # these add_subparser objects return objects to which arguments could be # attached, but they have annoying arg ordering constrains so we use # groups instead: https://github.com/letsencrypt/letsencrypt/issues/820 - for v, func in VERBS.items(): - add_subparser(v, func) + for v in VERBS: + add_subparser(v) helpful.add_group("auth", description="Options for modifying how a cert is obtained") helpful.add_group("install", description="Options for modifying how a cert is deployed") diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index f5613ee58..9a99a74cc 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -44,8 +44,6 @@ class CLITest(unittest.TestCase): def test_no_flags(self): with mock.patch('letsencrypt.cli.run') as mock_run: - from letsencrypt import cli - cli.VERBS["run"] = mock_run self._call([]) self.assertEqual(1, mock_run.call_count) From 95c4b55da09aee285bd823bf993d43089907456a Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 30 Sep 2015 16:49:03 -0700 Subject: [PATCH 096/145] Mark Nginx as non-working. --- letsencrypt-nginx/letsencrypt_nginx/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 3f6d6f327..a88607e58 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -56,7 +56,7 @@ class NginxConfigurator(common.Plugin): zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) zope.interface.classProvides(interfaces.IPluginFactory) - description = "Nginx Web Server - Alpha" + description = "Nginx Web Server - currently doesn't work" @classmethod def add_parser_arguments(cls, add): From 11ca1108c2536adb0d735e76829f178f06a08715 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 16:53:08 -0700 Subject: [PATCH 097/145] Test cases for command line help --- letsencrypt/tests/cli_test.py | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 9a99a74cc..75eec1978 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -2,6 +2,7 @@ import itertools import os import shutil +import StringIO import traceback import tempfile import unittest @@ -42,6 +43,21 @@ class CLITest(unittest.TestCase): ret = cli.main(args) return ret, stdout, stderr, client + def _call_stdout(self, args): + """ + Variant of _call that preserves stdout so that it can be mocked by the + caller. + """ + from letsencrypt import cli + args = ['--text', '--config-dir', self.config_dir, + '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, + '--agree-eula'] + args + with mock.patch('letsencrypt.cli.sys.stderr') as stderr: + with mock.patch('letsencrypt.cli.client') as client: + ret = cli.main(args) + return ret, None, stderr, client + + def test_no_flags(self): with mock.patch('letsencrypt.cli.run') as mock_run: self._call([]) @@ -49,7 +65,26 @@ class CLITest(unittest.TestCase): def test_help(self): self.assertRaises(SystemExit, self._call, ['--help']) - self.assertRaises(SystemExit, self._call, ['--help all']) + self.assertRaises(SystemExit, self._call, ['--help', 'all']) + output = StringIO.StringIO() + with mock.patch('letsencrypt.cli.sys.stdout', new=output): + self.assertRaises(SystemExit, self._call_stdout, ['--help', 'all']) + out = output.getvalue() + self.assertTrue("--configurator" in out) + self.assertTrue("how a cert is deployed" in out) + self.assertTrue("--manual-test-mode" in out) + output.truncate(0) + self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx']) + out = output.getvalue() + self.assertTrue("--nginx-ctl" in out) + self.assertTrue("--manual-test-mode" not in out) + self.assertTrue("--checkpoints" not in out) + output.truncate(0) + self.assertRaises(SystemExit, self._call_stdout, ['--help', 'plugins']) + out = output.getvalue() + self.assertTrue("--manual-test-mode" not in out) + self.assertTrue("--prepare" in out) + self.assertTrue("Plugin options" in out) def test_rollback(self): _, _, _, client = self._call(['rollback']) From 43cb36807a001562726bceaaaae00d708fcc5ed2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 17:00:09 -0700 Subject: [PATCH 098/145] Also test top level help --- letsencrypt/tests/cli_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 75eec1978..0a92aba62 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -85,6 +85,12 @@ class CLITest(unittest.TestCase): self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--prepare" in out) self.assertTrue("Plugin options" in out) + output.truncate(0) + self.assertRaises(SystemExit, self._call_stdout, ['-h']) + out = output.getvalue() + from letsencrypt import cli + self.assertTrue(cli.USAGE in out) + def test_rollback(self): _, _, _, client = self._call(['rollback']) From 9cf2ea8a5742d8868f1f1c47377626a741464bc7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 30 Sep 2015 17:16:27 -0700 Subject: [PATCH 099/145] Report Apache correctly when uninstalled --- .../letsencrypt_apache/configurator.py | 6 +++ .../tests/configurator_test.py | 10 ++++- .../letsencrypt_apache/tests/util.py | 45 ++++++++++--------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index ad3c62d2c..f3d2b5f9a 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -137,6 +137,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :raises .errors.PluginError: If there is any other error """ + # Verify Apache is installed + for exe in (self.conf("ctl"), self.conf("enmod"), + self.conf("dismod"), self.conf("init-script")): + if not le_util.exe_exists(exe): + raise errors.NoInstallationError + # Make sure configuration is valid self.config_test() diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 026594a8f..7c2137c45 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -37,8 +37,16 @@ class TwoVhost80Test(util.ApacheTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) + @mock.patch("letsencrypt_apache.configurator.le_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("letsencrypt_apache.parser.ApacheParser") - def test_prepare_version(self, _): + @mock.patch("letsencrypt_apache.configurator.le_util.exe_exists") + def test_prepare_version(self, mock_exe_exists, _): + mock_exe_exists.return_value = True self.config.version = None self.config.config_test = mock.Mock() self.config.get_version = mock.Mock(return_value=(1, 1)) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/util.py b/letsencrypt-apache/letsencrypt_apache/tests/util.py index b544e06ee..2594ba773 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/util.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/util.py @@ -66,31 +66,34 @@ def get_apache_configurator( """ backups = os.path.join(work_dir, "backups") + mock_le_config = mock.MagicMock( + apache_server_root=config_path, + apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"], + backup_dir=backups, + config_dir=config_dir, + temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), + in_progress_dir=os.path.join(backups, "IN_PROGRESS"), + work_dir=work_dir) with mock.patch("letsencrypt_apache.configurator." "subprocess.Popen") as mock_popen: - with mock.patch("letsencrypt_apache.parser.ApacheParser." - "update_runtime_variables"): - # This indicates config_test passes - mock_popen().communicate.return_value = ("Fine output", "No problems") - mock_popen().returncode = 0 + # This indicates config_test passes + mock_popen().communicate.return_value = ("Fine output", "No problems") + mock_popen().returncode = 0 + with mock.patch("letsencrypt_apache.configurator.le_util." + "exe_exists") as mock_exe_exists: + mock_exe_exists.return_value = True + with mock.patch("letsencrypt_apache.parser.ApacheParser." + "update_runtime_variables"): + config = configurator.ApacheConfigurator( + config=mock_le_config, + name="apache", + version=version) + # This allows testing scripts to set it a bit more quickly + if conf is not None: + config.conf = conf # pragma: no cover - config = configurator.ApacheConfigurator( - config=mock.MagicMock( - apache_server_root=config_path, - apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"], - backup_dir=backups, - config_dir=config_dir, - temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), - in_progress_dir=os.path.join(backups, "IN_PROGRESS"), - work_dir=work_dir), - name="apache", - version=version) - # This allows testing scripts to set it a bit more quickly - if conf is not None: - config.conf = conf # pragma: no cover - - config.prepare() + config.prepare() return config From 7a7971ff8d04c87219e31e27e152eb712209cd91 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 19:06:16 -0700 Subject: [PATCH 100/145] Make sure the LICENSE file is accurate for first pre-relase - In general copyrights remain with their respective authors or authors' organizations, but with license granted by clause 5 of the Apache License. - Presently the plurality of the copyright in the client is held by EFF as a result of work-for-hire by jdkasten, jdkasten, bmw, schoen, pde, rolandshoemaker and jsha; or by Jakub Warmuz or his employer, Google. --- LICENSE.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 5a9f6fa55..2ed752521 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,5 @@ -Let's Encrypt: -Copyright (c) Internet Security Research Group +Let's Encrypt Python Client +Copyright (c) Electronic Frontier Foundation and others Licensed Apache Version 2.0 Incorporating code from nginxparser From 8041b35f9988d1193528df0d36b14eca35babc3a Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 30 Sep 2015 19:06:16 -0700 Subject: [PATCH 101/145] Make sure the LICENSE file is accurate for first pre-relase - In general copyrights remain with their respective authors or authors' organizations, but with license granted by clause 5 of the Apache License. - Presently the plurality of the copyright in the client is held by EFF as a result of work-for-hire by jdkasten, bmw, schoen, pde, rolandshoemaker and jsha; or by Jakub Warmuz or his employer, Google. --- LICENSE.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 5a9f6fa55..2ed752521 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,5 @@ -Let's Encrypt: -Copyright (c) Internet Security Research Group +Let's Encrypt Python Client +Copyright (c) Electronic Frontier Foundation and others Licensed Apache Version 2.0 Incorporating code from nginxparser From 268368b3e928e669420beeefd5d82a8af4de4a1f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 1 Oct 2015 10:12:38 -0700 Subject: [PATCH 102/145] Updated README to reflect state of Nginx plugin --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 23e4dad29..43ecd413c 100644 --- a/README.rst +++ b/README.rst @@ -79,7 +79,7 @@ Current Features * web servers supported: - apache/2.x (tested and working on Ubuntu Linux) - - nginx/0.8.48+ (tested and mostly working on Ubuntu Linux) + - nginx/0.8.48+ (under development) - standalone (runs its own webserver to prove you control the domain) * the private key is generated locally on your system From 6bde83c9835b1fba9a935f341e62a48b8393d189 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 1 Oct 2015 11:53:11 -0700 Subject: [PATCH 103/145] Fixed indentation in storage.py --- letsencrypt/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 08dff25a1..be270a762 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -520,7 +520,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes remaining = expiry - now if remaining < autorenew_interval: return True - return False + return False @classmethod def new_lineage(cls, lineagename, cert, privkey, chain, From c976c0abdfdbeb7edc580b252243d94c5c0296ce Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 1 Oct 2015 13:03:23 -0700 Subject: [PATCH 104/145] Removed duplicated code --- letsencrypt/client.py | 14 +++++--------- letsencrypt/storage.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index c82131af3..7a78add38 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -268,19 +268,15 @@ class Client(object): :param .RenewableCert cert: Newly issued certificate """ - if ("autorenew" not in cert.configuration or - cert.configuration.as_bool("autorenew")): - if ("autodeploy" not in cert.configuration or - cert.configuration.as_bool("autodeploy")): + if cert.autorenewal_is_enabled(): + if cert.autodeployment_is_enabled(): msg = "Automatic renewal and deployment has " else: msg = "Automatic renewal but not automatic deployment has " + elif cert.autodeployment_is_enabled(): + msg = "Automatic deployment but not automatic renewal has " else: - if ("autodeploy" not in cert.configuration or - cert.configuration.as_bool("autodeploy")): - msg = "Automatic deployment but not automatic renewal has " - else: - msg = "Automatic renewal and deployment has not " + msg = "Automatic renewal and deployment has not " msg += ("been enabled for your certificate. These settings can be " "configured in the directories under {0}.").format( diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 08dff25a1..435bb29d5 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -439,6 +439,18 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes with open(target) as f: return crypto_util.get_sans_from_cert(f.read()) + def autodeployment_is_enabled(self): + """Is automatic deployment enabled for this cert? + + If autodeploy is not specified, defaults to True. + + :returns: True if automatic deployment is enabled + :rtype: bool + + """ + return ("autodeploy" not in self.configuration or + self.configuration.as_bool("autodeploy")) + def should_autodeploy(self): """Should this lineage now automatically deploy a newer version? @@ -453,8 +465,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :rtype: bool """ - if ("autodeploy" not in self.configuration or - self.configuration.as_bool("autodeploy")): + if self.autodeployment_is_enabled(): if self.has_pending_deployment(): interval = self.configuration.get("deploy_before_expiry", "5 days") @@ -488,6 +499,18 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # certificate is not revoked). return False + def autorenewal_is_enabled(self): + """Is automatic renewal enabled for this cert? + + If autorenew is not specified, defaults to True. + + :returns: True if automatic renewal is enabled + :rtype: bool + + """ + return ("autorenew" not in self.configuration or + self.configuration.as_bool("autorenew")) + def should_autorenew(self): """Should we now try to autorenew the most recent cert version? @@ -504,8 +527,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :rtype: bool """ - if ("autorenew" not in self.configuration or - self.configuration.as_bool("autorenew")): + if self.autorenewal_is_enabled(): # Consider whether to attempt to autorenew this cert now # Renewals on the basis of revocation From 59348ad30c4276fc6fb89487e659d24db23dc453 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 1 Oct 2015 13:33:22 -0700 Subject: [PATCH 105/145] Made methods private and updated tests --- letsencrypt/storage.py | 8 ++++---- letsencrypt/tests/client_test.py | 15 ++++++--------- letsencrypt/tests/renewer_test.py | 32 +++++++++++++++++-------------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 435bb29d5..ceae4bfa9 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -129,7 +129,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes self.chain = self.configuration["chain"] self.fullchain = self.configuration["fullchain"] - def consistent(self): + def _consistent(self): """Are the files associated with this lineage self-consistent? :returns: Whether the files stored in connection with this @@ -187,7 +187,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # for x in ALL_FOUR))) == 1 return True - def fix(self): + def _fix(self): """Attempt to fix defects or inconsistencies in this lineage. .. todo:: Currently unimplemented. @@ -347,7 +347,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes smallest_current = min(self.current_version(x) for x in ALL_FOUR) return smallest_current < self.latest_common_version() - def update_link_to(self, kind, version): + def _update_link_to(self, kind, version): """Make the specified item point at the specified version. (Note that this method doesn't verify that the specified version @@ -379,7 +379,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param int version: the desired version""" for kind in ALL_FOUR: - self.update_link_to(kind, version) + self._update_link_to(kind, version) def _notafterbefore(self, method, version): """Internal helper function for finding notbefore/notafter.""" diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1a232bccb..1e63bdbb6 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -4,14 +4,12 @@ import shutil import tempfile import unittest -import configobj import OpenSSL import mock from acme import jose from letsencrypt import account -from letsencrypt import configuration from letsencrypt import errors from letsencrypt import le_util @@ -120,29 +118,28 @@ class ClientTest(unittest.TestCase): def test_report_renewal_status(self, mock_zope): # pylint: disable=protected-access cert = mock.MagicMock() - cert.configuration = configobj.ConfigObj() - cert.cli_config = configuration.RenewerConfiguration(self.config) + cert.cli_config.renewal_configs_dir = "/foo/bar/baz" - cert.configuration["autorenew"] = "True" - cert.configuration["autodeploy"] = "True" + cert.autorenewal_is_enabled.return_value = True + cert.autodeployment_is_enabled.return_value = True self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal and deployment has been" in msg) self.assertTrue(cert.cli_config.renewal_configs_dir in msg) - cert.configuration["autorenew"] = "False" + cert.autorenewal_is_enabled.return_value = False self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("deployment but not automatic renewal" in msg) self.assertTrue(cert.cli_config.renewal_configs_dir in msg) - cert.configuration["autodeploy"] = "False" + cert.autodeployment_is_enabled.return_value = False self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal and deployment has not" in msg) self.assertTrue(cert.cli_config.renewal_configs_dir in msg) - cert.configuration["autorenew"] = "True" + cert.autorenewal_is_enabled.return_value = True self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal but not automatic deployment" in msg) diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index e67631605..b7b2843d5 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -123,46 +123,47 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertRaises( errors.CertStorageError, storage.RenewableCert, config, defaults) - def test_consistent(self): # pylint: disable=too-many-statements + def test_consistent(self): + # pylint: disable=too-many-statements,protected-access oldcert = self.test_rc.cert self.test_rc.cert = "relative/path" # Absolute path for item requirement - self.assertFalse(self.test_rc.consistent()) + self.assertFalse(self.test_rc._consistent()) self.test_rc.cert = oldcert # Items must exist requirement - self.assertFalse(self.test_rc.consistent()) + self.assertFalse(self.test_rc._consistent()) # Items must be symlinks requirements fill_with_sample_data(self.test_rc) - self.assertFalse(self.test_rc.consistent()) + self.assertFalse(self.test_rc._consistent()) unlink_all(self.test_rc) # Items must point to desired place if they are relative for kind in ALL_FOUR: os.symlink(os.path.join("..", kind + "17.pem"), getattr(self.test_rc, kind)) - self.assertFalse(self.test_rc.consistent()) + self.assertFalse(self.test_rc._consistent()) unlink_all(self.test_rc) # Items must point to desired place if they are absolute for kind in ALL_FOUR: os.symlink(os.path.join(self.tempdir, kind + "17.pem"), getattr(self.test_rc, kind)) - self.assertFalse(self.test_rc.consistent()) + self.assertFalse(self.test_rc._consistent()) unlink_all(self.test_rc) # Items must point to things that exist for kind in ALL_FOUR: os.symlink(os.path.join("..", "..", "archive", "example.org", kind + "17.pem"), getattr(self.test_rc, kind)) - self.assertFalse(self.test_rc.consistent()) + self.assertFalse(self.test_rc._consistent()) # This version should work fill_with_sample_data(self.test_rc) - self.assertTrue(self.test_rc.consistent()) + self.assertTrue(self.test_rc._consistent()) # Items must point to things that follow the naming convention os.unlink(self.test_rc.fullchain) os.symlink(os.path.join("..", "..", "archive", "example.org", "fullchain_17.pem"), self.test_rc.fullchain) with open(self.test_rc.fullchain, "w") as f: f.write("wrongly-named fullchain") - self.assertFalse(self.test_rc.consistent()) + self.assertFalse(self.test_rc._consistent()) def test_current_target(self): # Relative path logic @@ -259,14 +260,15 @@ class RenewableCertTests(BaseRenewableCertTest): with open(where, "w") as f: f.write(kind) self.assertEqual(ver, self.test_rc.current_version(kind)) - self.test_rc.update_link_to("cert", 3) - self.test_rc.update_link_to("privkey", 2) + # pylint: disable=protected-access + self.test_rc._update_link_to("cert", 3) + self.test_rc._update_link_to("privkey", 2) self.assertEqual(3, self.test_rc.current_version("cert")) self.assertEqual(2, self.test_rc.current_version("privkey")) self.assertEqual(5, self.test_rc.current_version("chain")) self.assertEqual(5, self.test_rc.current_version("fullchain")) # Currently we are allowed to update to a version that doesn't exist - self.test_rc.update_link_to("chain", 3000) + self.test_rc._update_link_to("chain", 3000) # However, current_version doesn't allow querying the resulting # version (because it's a broken link). self.assertEqual(os.path.basename(os.readlink(self.test_rc.chain)), @@ -507,7 +509,8 @@ class RenewableCertTests(BaseRenewableCertTest): self.defaults, self.cli_config) # This consistency check tests most relevant properties about the # newly created cert lineage. - self.assertTrue(result.consistent()) + # pylint: disable=protected-access + self.assertTrue(result._consistent()) self.assertTrue(os.path.exists(os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) with open(result.fullchain) as f: @@ -578,9 +581,10 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertRaises( errors.CertStorageError, self.test_rc.newest_available_version, "elephant") + # pylint: disable=protected-access self.assertRaises( errors.CertStorageError, - self.test_rc.update_link_to, "elephant", 17) + self.test_rc._update_link_to, "elephant", 17) def test_ocsp_revoked(self): # XXX: This is currently hardcoded to False due to a lack of an From d7a16ecfcb76d50702375b3dbb66669e59818ddc Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 1 Oct 2015 15:39:55 -0700 Subject: [PATCH 106/145] Added tests and documentation --- letsencrypt/error_handler.py | 5 +++-- letsencrypt/tests/error_handler_test.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py index 1292f2bc5..8b0eb7c8b 100644 --- a/letsencrypt/error_handler.py +++ b/letsencrypt/error_handler.py @@ -22,8 +22,8 @@ class ErrorHandler(object): """Registers functions to be called if an exception or signal occurs. This class allows you to register functions that will be called when - an exception or signal is encountered. The class works best as a - context manager. For example: + an exception (excluding SystemExit) or signal is encountered. The + class works best as a context manager. For example: with ErrorHandler(cleanup_func): do_something() @@ -50,6 +50,7 @@ class ErrorHandler(object): self.set_signal_handlers() def __exit__(self, exec_type, exec_value, trace): + # SystemExit is ignored to properly handle forks that don't exec if exec_type not in (None, SystemExit): logger.debug("Encountered exception:\n%s", "".join( traceback.format_exception(exec_type, exec_value, trace))) diff --git a/letsencrypt/tests/error_handler_test.py b/letsencrypt/tests/error_handler_test.py index 66acac930..c92f12435 100644 --- a/letsencrypt/tests/error_handler_test.py +++ b/letsencrypt/tests/error_handler_test.py @@ -1,5 +1,6 @@ """Tests for letsencrypt.error_handler.""" import signal +import sys import unittest import mock @@ -50,6 +51,14 @@ class ErrorHandlerTest(unittest.TestCase): self.init_func.assert_called_once_with() bad_func.assert_called_once_with() + def test_sysexit_ignored(self): + try: + with self.handler: + sys.exit(0) + except SystemExit: + pass + self.assertFalse(self.init_func.called) + if __name__ == "__main__": unittest.main() # pragma: no cover From 895faa7dc94b554881b4330f20354670c276df19 Mon Sep 17 00:00:00 2001 From: Brandon Kreisel Date: Fri, 2 Oct 2015 22:36:56 -0400 Subject: [PATCH 107/145] Add OS X bootstrap for integration enviornment Installs requirements and sets up environment to run boulder and integration tests --- tests/mac-bootstrap.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100755 tests/mac-bootstrap.sh diff --git a/tests/mac-bootstrap.sh b/tests/mac-bootstrap.sh new file mode 100755 index 000000000..38db6a969 --- /dev/null +++ b/tests/mac-bootstrap.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +#Check Homebrew +if ! hash brew 2>/dev/null; then + echo "Homebrew Not Installed\nDownloading..." + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +fi + +brew install libtool mariadb rabbitmq coreutils go + +mysql.server start + +rabbit_pid=`ps | grep rabbitmq | grep -v grep | awk '{ print $1}'` +if [ -n rabbit_pid ]; then + echo "RabbitMQ already running" +else + rabbitmq-server & +fi + +hosts_entry=`cat /etc/hosts | grep "127.0.0.1 le.wtf"` +if [ -z hosts_entry ]; then + echo "Adding hosts entry for le.wtf..." + sudo sh -c "echo 127.0.0.1 le.wtf >> /etc/hosts" +fi + +./tests/boulder-start.sh From 0868a5962f4ae812f9bcb1b39e4c3bace207074b Mon Sep 17 00:00:00 2001 From: Brandon Kreisel Date: Fri, 2 Oct 2015 22:37:22 -0400 Subject: [PATCH 108/145] Add documentation for OS X bootstrap script --- docs/contributing.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index c6443e3b2..7b0768efb 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -67,6 +67,8 @@ The following tools are there to help you: Integration ~~~~~~~~~~~ +Mac OS X users: Run `./tests/mac-integration.sh` to configure the +integration tests environment and start boulder First, install `Go`_ 1.5, libtool-ltdl, mariadb-server and rabbitmq-server and then start Boulder_, an ACME CA server:: From 8409c9c658b247421caef540d57410dc8a00ef41 Mon Sep 17 00:00:00 2001 From: Brandon Kreisel Date: Sat, 3 Oct 2015 11:27:39 -0400 Subject: [PATCH 109/145] Meddle with more documentation and learn how to bash flag --- docs/contributing.rst | 6 +++--- tests/mac-bootstrap.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 7b0768efb..c746c6ae7 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -67,10 +67,10 @@ The following tools are there to help you: Integration ~~~~~~~~~~~ -Mac OS X users: Run `./tests/mac-integration.sh` to configure the -integration tests environment and start boulder +Mac OS X users: Run `./tests/mac-bootstrap.sh` instead of `boulder-start.sh` to +install dependencies, configure the environment, and start boulder. -First, install `Go`_ 1.5, libtool-ltdl, mariadb-server and +Otherwise, install `Go`_ 1.5, libtool-ltdl, mariadb-server and rabbitmq-server and then start Boulder_, an ACME CA server:: ./tests/boulder-start.sh diff --git a/tests/mac-bootstrap.sh b/tests/mac-bootstrap.sh index 38db6a969..66036ce56 100755 --- a/tests/mac-bootstrap.sh +++ b/tests/mac-bootstrap.sh @@ -11,14 +11,14 @@ brew install libtool mariadb rabbitmq coreutils go mysql.server start rabbit_pid=`ps | grep rabbitmq | grep -v grep | awk '{ print $1}'` -if [ -n rabbit_pid ]; then +if [ -n "$rabbit_pid" ]; then echo "RabbitMQ already running" else rabbitmq-server & fi hosts_entry=`cat /etc/hosts | grep "127.0.0.1 le.wtf"` -if [ -z hosts_entry ]; then +if [ -z "$hosts_entry" ]; then echo "Adding hosts entry for le.wtf..." sudo sh -c "echo 127.0.0.1 le.wtf >> /etc/hosts" fi From b89bd4b5def4c2888e522caef6f0f039c6e8c601 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 4 Oct 2015 06:24:24 +0000 Subject: [PATCH 110/145] Add API docs for letshelp_letsencrypt --- docs/pkgs/letshelp_letsencrypt.rst | 11 +++++++++++ readthedocs.org.requirements.txt | 1 + 2 files changed, 12 insertions(+) create mode 100644 docs/pkgs/letshelp_letsencrypt.rst diff --git a/docs/pkgs/letshelp_letsencrypt.rst b/docs/pkgs/letshelp_letsencrypt.rst new file mode 100644 index 000000000..8f6872eac --- /dev/null +++ b/docs/pkgs/letshelp_letsencrypt.rst @@ -0,0 +1,11 @@ +:mod:`letshelp_letsencrypt` +--------------------------- + +.. automodule:: letshelp_letsencrypt + :members: + +:mod:`letshelp_letsencrypt.apache` +================================== + +.. automodule:: letshelp_letsencrypt.apache + :members: diff --git a/readthedocs.org.requirements.txt b/readthedocs.org.requirements.txt index f686b00bf..d895f80a5 100644 --- a/readthedocs.org.requirements.txt +++ b/readthedocs.org.requirements.txt @@ -11,3 +11,4 @@ -e .[docs] -e letsencrypt-apache -e letsencrypt-nginx +-e letshelp-letsencrypt From 08afe48019639775bee4b1a30bfccabb18309bd6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 4 Oct 2015 06:37:35 +0000 Subject: [PATCH 111/145] Add API docs for letsencrypt_compatibility_test --- docs/pkgs/letsencrypt_compatibility_test.rst | 53 ++++++++++++++++++++ readthedocs.org.requirements.txt | 1 + 2 files changed, 54 insertions(+) create mode 100644 docs/pkgs/letsencrypt_compatibility_test.rst diff --git a/docs/pkgs/letsencrypt_compatibility_test.rst b/docs/pkgs/letsencrypt_compatibility_test.rst new file mode 100644 index 000000000..f792a2cc3 --- /dev/null +++ b/docs/pkgs/letsencrypt_compatibility_test.rst @@ -0,0 +1,53 @@ +:mod:`letsencrypt_compatibility_test` +------------------------------------- + +.. automodule:: letsencrypt_compatibility_test + :members: + +:mod:`letsencrypt_compatibility_test.errors` +============================================ + +.. automodule:: letsencrypt_compatibility_test.errors + :members: + +:mod:`letsencrypt_compatibility_test.interfaces` +================================================ + +.. automodule:: letsencrypt_compatibility_test.interfaces + :members: + +:mod:`letsencrypt_compatibility_test.test_driver` +================================================= + +.. automodule:: letsencrypt_compatibility_test.test_driver + :members: + +:mod:`letsencrypt_compatibility_test.util` +========================================== + +.. automodule:: letsencrypt_compatibility_test.util + :members: + +:mod:`letsencrypt_compatibility_test.configurators` +=================================================== + +.. automodule:: letsencrypt_compatibility_test.configurators + :members: + +:mod:`letsencrypt_compatibility_test.configurators.apache` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: letsencrypt_compatibility_test.configurators.apache + :members: + +:mod:`letsencrypt_compatibility_test.configurators.apache.apache24` +------------------------------------------------------------------- + +.. automodule:: letsencrypt_compatibility_test.configurators.apache.apache24 + :members: + +:mod:`letsencrypt_compatibility_test.configurators.apache.common` +------------------------------------------------------------------- + +.. automodule:: letsencrypt_compatibility_test.configurators.apache.common + :members: diff --git a/readthedocs.org.requirements.txt b/readthedocs.org.requirements.txt index d895f80a5..3c3a3c576 100644 --- a/readthedocs.org.requirements.txt +++ b/readthedocs.org.requirements.txt @@ -11,4 +11,5 @@ -e .[docs] -e letsencrypt-apache -e letsencrypt-nginx +-e letsencrypt-compatibility-test -e letshelp-letsencrypt From 4925e0b811616915c9c7752d506eab2739374ba3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 4 Oct 2015 07:47:14 +0000 Subject: [PATCH 112/145] Update MANIFEST.in for compatibility-test --- letsencrypt-compatibility-test/MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/letsencrypt-compatibility-test/MANIFEST.in b/letsencrypt-compatibility-test/MANIFEST.in index a6aa14443..225d19085 100644 --- a/letsencrypt-compatibility-test/MANIFEST.in +++ b/letsencrypt-compatibility-test/MANIFEST.in @@ -1 +1,4 @@ +include letsencrypt_compatibility_test/configurators/apache/a2enmod.sh +include letsencrypt_compatibility_test/configurators/apache/a2dismod.sh +include letsencrypt_compatibility_test/configurators/apache/Dockerfile recursive-include letsencrypt_compatibility_test/testdata * From d20088a43532ec503ecc3f7f7e34eba2ee120cf3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 4 Oct 2015 08:55:13 +0000 Subject: [PATCH 113/145] docs: pip install -U setuptools pip --- docs/using.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index cfce29bae..16a551c1b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -121,11 +121,12 @@ Installation ============ .. "pip install acme" doesn't search for "acme" in cwd, just like "pip - install -e acme" does + install -e acme" does; `-U setuptools pip` necessary for #722 .. code-block:: shell virtualenv --no-site-packages -p python2 venv + ./venv/bin/pip install -U setuptools pip ./venv/bin/pip install -r requirements.txt acme/ . letsencrypt-apache/ letsencrypt-nginx/ .. warning:: Please do **not** use ``python setup.py install``. Please From 5d4e1b68cddc0a45e24adbff2d56319b572fec31 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 4 Oct 2015 08:57:53 +0000 Subject: [PATCH 114/145] autospec=False for socket.socket (quick-fixes: #779). Also, https://github.com/testing-cabal/mock/issues/323 --- letsencrypt/plugins/manual_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index cfe47b833..78bc4ae0e 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -68,7 +68,7 @@ class AuthenticatorTest(unittest.TestCase): mock_popen.side_effect = OSError self.assertEqual([False], self.auth_test_mode.perform(self.achalls)) - @mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True) + @mock.patch("letsencrypt.plugins.manual.socket.socket") @mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True) @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_command_run_failure( @@ -78,7 +78,7 @@ class AuthenticatorTest(unittest.TestCase): self.assertRaises( errors.Error, self.auth_test_mode.perform, self.achalls) - @mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True) + @mock.patch("letsencrypt.plugins.manual.socket.socket") @mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True) @mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify", autospec=True) From 7644613171e468e66eee6797dc2de3ea8ab9d8ac Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 4 Oct 2015 10:10:41 +0000 Subject: [PATCH 115/145] Update Copyright notice in subpackages LICENSE. This corresponds to changes in #871. --- acme/LICENSE.txt | 2 +- letsencrypt-apache/LICENSE.txt | 2 +- letsencrypt-compatibility-test/LICENSE.txt | 2 +- letsencrypt-nginx/LICENSE.txt | 2 +- letshelp-letsencrypt/LICENSE.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/acme/LICENSE.txt b/acme/LICENSE.txt index 7c13afb9d..981c46c9f 100644 --- a/acme/LICENSE.txt +++ b/acme/LICENSE.txt @@ -1,4 +1,4 @@ - Copyright 2015 Internet Security Research Group + Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/letsencrypt-apache/LICENSE.txt b/letsencrypt-apache/LICENSE.txt index 7c13afb9d..981c46c9f 100644 --- a/letsencrypt-apache/LICENSE.txt +++ b/letsencrypt-apache/LICENSE.txt @@ -1,4 +1,4 @@ - Copyright 2015 Internet Security Research Group + Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/letsencrypt-compatibility-test/LICENSE.txt b/letsencrypt-compatibility-test/LICENSE.txt index 7c13afb9d..981c46c9f 100644 --- a/letsencrypt-compatibility-test/LICENSE.txt +++ b/letsencrypt-compatibility-test/LICENSE.txt @@ -1,4 +1,4 @@ - Copyright 2015 Internet Security Research Group + Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/letsencrypt-nginx/LICENSE.txt b/letsencrypt-nginx/LICENSE.txt index 7c13afb9d..981c46c9f 100644 --- a/letsencrypt-nginx/LICENSE.txt +++ b/letsencrypt-nginx/LICENSE.txt @@ -1,4 +1,4 @@ - Copyright 2015 Internet Security Research Group + Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/letshelp-letsencrypt/LICENSE.txt b/letshelp-letsencrypt/LICENSE.txt index 7c13afb9d..981c46c9f 100644 --- a/letshelp-letsencrypt/LICENSE.txt +++ b/letshelp-letsencrypt/LICENSE.txt @@ -1,4 +1,4 @@ - Copyright 2015 Internet Security Research Group + Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 32da607ae5580a164933b17e32599faefbe9b396 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 10:39:38 -0500 Subject: [PATCH 116/145] crypto_util: Remove asn1_generalizedtime_to_dt(...) Not used by any other code AFAIK (ack'd entire codebase). --- letsencrypt/crypto_util.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 79cd24ed6..73e4f9bdd 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -274,11 +274,6 @@ def asn1_generalizedtime_to_dt(timestamp): return datetime.datetime.strptime(timestamp, '%Y%m%d%H%M%SZ') -def pyopenssl_x509_name_as_text(x509name): - """Convert `OpenSSL.crypto.X509Name` to text.""" - return "/".join("{0}={1}" for key, value in x509name.get_components()) - - def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. From 6994dad59b37d26007f14030558d560144bce81e Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 11:17:07 -0500 Subject: [PATCH 117/145] crypto_util: Remove `asn1_generalizedtime_to_dt(...)` --- letsencrypt/crypto_util.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 73e4f9bdd..1007f18c7 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -261,19 +261,6 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): csr, OpenSSL.crypto.load_certificate_request, typ) -def asn1_generalizedtime_to_dt(timestamp): - """Convert ASN.1 GENERALIZEDTIME to datetime. - - Useful for deserialization of `OpenSSL.crypto.X509.get_notAfter` and - `OpenSSL.crypto.X509.get_notAfter` outputs. - - .. todo:: This function support only one format: `%Y%m%d%H%M%SZ`. - Implement remaining two. - - """ - return datetime.datetime.strptime(timestamp, '%Y%m%d%H%M%SZ') - - def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. From 884d8e9905527d6df4f4bf42954763f4469fe788 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 11:28:03 -0500 Subject: [PATCH 118/145] crypto_util: Remove unused import --- letsencrypt/crypto_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 1007f18c7..2f24c4fb6 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -4,7 +4,6 @@ is capable of handling the signatures. """ -import datetime import logging import os From 032f3e8f642a9edde35d0cfa8fdbc30a70ab5c4d Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 15:00:57 -0500 Subject: [PATCH 119/145] crypto_util: test _pyopenssl_load --- letsencrypt/tests/crypto_util_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index b4d2aa394..91c1160a0 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -212,6 +212,17 @@ class GetSANsFromCSRTest(unittest.TestCase): self.assertEqual( [], self._call(test_util.load_vector('csr-nosans.pem'))) +class PyOpenSslLoaderTest(unittest.TestCase): + def test_pyopenssl_load(self): + from letsencrypt.crypto_util import _pyopenssl_load + + method_mock = mock.MagicMock() + fake_data = "this is test data" + fake_types = ('fake loading type',) + _pyopenssl_load(fake_data, method_mock, fake_types) + method_mock.assert_called_with(fake_types[0], fake_data) + + if __name__ == '__main__': unittest.main() # pragma: no cover From 7b2d40ce552c89a9d2c7bf1a94be66372497a006 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 15:34:57 -0500 Subject: [PATCH 120/145] crypto_util: test pyopenssl_load_certificate(...) --- letsencrypt/tests/crypto_util_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index 91c1160a0..9ad36c83b 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -222,7 +222,13 @@ class PyOpenSslLoaderTest(unittest.TestCase): _pyopenssl_load(fake_data, method_mock, fake_types) method_mock.assert_called_with(fake_types[0], fake_data) +class CertLoaderTest(unittest.TestCase): + def test_it(self): + from letsencrypt.crypto_util import pyopenssl_load_certificate + cert, file_type = pyopenssl_load_certificate(CERT) + self.assertEqual(cert.digest('sha1'), + OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, CERT).digest('sha1')) if __name__ == '__main__': unittest.main() # pragma: no cover From aa15fae11d9ec80ecce250c53d5028ed875885d3 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 15:37:10 -0500 Subject: [PATCH 121/145] crypto_util: merge _pyopenssl_load into pyopenssl_load_certificate --- letsencrypt/crypto_util.py | 24 +++++++++++------------- letsencrypt/tests/crypto_util_test.py | 12 +++--------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 79cd24ed6..eac0530aa 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -201,25 +201,23 @@ def valid_privkey(privkey): return False -def _pyopenssl_load(data, method, types=( - OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)): - openssl_errors = [] - for filetype in types: - try: - return method(filetype, data), filetype - except OpenSSL.crypto.Error as error: # TODO: anything else? - openssl_errors.append(error) - raise errors.Error("Unable to load: {0}".format(",".join( - str(error) for error in openssl_errors))) - - def pyopenssl_load_certificate(data): """Load PEM/DER certificate. :raises errors.Error: """ - return _pyopenssl_load(data, OpenSSL.crypto.load_certificate) + + openssl_errors = [] + + for file_type in (OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1): + try: + return OpenSSL.crypto.load_certificate(file_type, data), file_type + except OpenSSL.crypto.Error as error: # TODO: other errors? + openssl_errors.append(error) + raise errors.Error("Unable to load: {0}".format(",".join( + str(error) for error in openssl_errors))) + def _get_sans_from_cert_or_req( diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index 9ad36c83b..8ac8a0adc 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -212,17 +212,10 @@ class GetSANsFromCSRTest(unittest.TestCase): self.assertEqual( [], self._call(test_util.load_vector('csr-nosans.pem'))) -class PyOpenSslLoaderTest(unittest.TestCase): - def test_pyopenssl_load(self): - from letsencrypt.crypto_util import _pyopenssl_load - - method_mock = mock.MagicMock() - fake_data = "this is test data" - fake_types = ('fake loading type',) - _pyopenssl_load(fake_data, method_mock, fake_types) - method_mock.assert_called_with(fake_types[0], fake_data) class CertLoaderTest(unittest.TestCase): + """Tests for letsencrypt.crypto_util.pyopenssl_load_certificate""" + def test_it(self): from letsencrypt.crypto_util import pyopenssl_load_certificate @@ -230,5 +223,6 @@ class CertLoaderTest(unittest.TestCase): self.assertEqual(cert.digest('sha1'), OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, CERT).digest('sha1')) + if __name__ == '__main__': unittest.main() # pragma: no cover From 7b50f5d9bf02c2cc69298c601098901a4741deb3 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 15:42:05 -0500 Subject: [PATCH 122/145] Make pep8 happy --- letsencrypt/crypto_util.py | 7 +++---- letsencrypt/tests/crypto_util_test.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index eac0530aa..777b4d006 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -213,15 +213,14 @@ def pyopenssl_load_certificate(data): for file_type in (OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1): try: return OpenSSL.crypto.load_certificate(file_type, data), file_type - except OpenSSL.crypto.Error as error: # TODO: other errors? + except OpenSSL.crypto.Error as error: # TODO: other errors? openssl_errors.append(error) raise errors.Error("Unable to load: {0}".format(",".join( 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 _get_sans_from_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) except OpenSSL.crypto.Error as error: diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index 8ac8a0adc..b0c8c4482 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -221,7 +221,7 @@ class CertLoaderTest(unittest.TestCase): cert, file_type = pyopenssl_load_certificate(CERT) self.assertEqual(cert.digest('sha1'), - OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, CERT).digest('sha1')) + OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, CERT).digest('sha1')) if __name__ == '__main__': From b3bd71b42446d4ea5762c568e4256058791e54bf Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 15:43:51 -0500 Subject: [PATCH 123/145] Use previously-returned file_type in assertion --- letsencrypt/tests/crypto_util_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index b0c8c4482..c5b298e85 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -221,7 +221,7 @@ class CertLoaderTest(unittest.TestCase): cert, file_type = pyopenssl_load_certificate(CERT) self.assertEqual(cert.digest('sha1'), - OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, CERT).digest('sha1')) + OpenSSL.crypto.load_certificate(file_type, CERT).digest('sha1')) if __name__ == '__main__': From 9c8f09ec43e9ab84a0434e0a4a9bd9c833d0937d Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 15:49:56 -0500 Subject: [PATCH 124/145] Test that loading an invalid cert throws an error --- letsencrypt/tests/crypto_util_test.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index c5b298e85..e5217505d 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -8,6 +8,7 @@ import OpenSSL import mock import zope.component +from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.tests import test_util @@ -216,13 +217,20 @@ class GetSANsFromCSRTest(unittest.TestCase): class CertLoaderTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.pyopenssl_load_certificate""" - def test_it(self): + def test_load_valid_cert(self): from letsencrypt.crypto_util import pyopenssl_load_certificate cert, file_type = pyopenssl_load_certificate(CERT) self.assertEqual(cert.digest('sha1'), OpenSSL.crypto.load_certificate(file_type, CERT).digest('sha1')) + def test_load_invalid_cert(self): + from letsencrypt.crypto_util import pyopenssl_load_certificate + bad_cert_data = CERT.replace("BEGIN CERTIFICATE", "ASDFASDFASDF!!!") + + with self.assertRaises(errors.Error): + cert, file_type = pyopenssl_load_certificate(bad_cert_data) + if __name__ == '__main__': unittest.main() # pragma: no cover From 917a6d63734b63400e9bcf038ccf7d62853ec254 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 15:56:40 -0500 Subject: [PATCH 125/145] Make lint happy, remove unused variables from crypto_util_test --- letsencrypt/tests/crypto_util_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index e5217505d..2e04c748a 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -229,7 +229,7 @@ class CertLoaderTest(unittest.TestCase): bad_cert_data = CERT.replace("BEGIN CERTIFICATE", "ASDFASDFASDF!!!") with self.assertRaises(errors.Error): - cert, file_type = pyopenssl_load_certificate(bad_cert_data) + pyopenssl_load_certificate(bad_cert_data) if __name__ == '__main__': From d5ebc38b33eabf19fa033d4c5b7b266be05639b8 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Sun, 4 Oct 2015 20:39:19 -0500 Subject: [PATCH 126/145] Fix pep8 warnings (down to only one now!) --- letsencrypt/cli.py | 25 ++++++++++++++----------- letsencrypt/tests/cli_test.py | 2 -- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 73dd24bdb..0bd5f537e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -729,11 +729,13 @@ def create_parser(plugins, args): return helpful.parser, helpful.args + # For now unfortunately this constant just needs to match the code below; # there isn't an elegant way to autogenerate it in time. VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", "plugins"] HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + VERBS + def _create_subparsers(helpful): subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") @@ -741,7 +743,7 @@ def _create_subparsers(helpful): if name == "plugins": func = plugins_cmd else: - func = eval(name) # pylint: disable=eval-used + func = eval(name) # pylint: disable=eval-used h = func.__doc__.splitlines()[0] subparser = subparsers.add_parser(name, help=h, description=func.__doc__) subparser.set_defaults(func=func) @@ -762,22 +764,23 @@ def _create_subparsers(helpful): helpful.add_group("plugins", description="Plugin options") helpful.add("auth", - "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER format.") + "--csr", type=read_file, + help="Path to a Certificate Signing Request (CSR) in DER format.") helpful.add("rollback", - "--checkpoints", type=int, metavar="N", - default=flag_default("rollback_checkpoints"), - help="Revert configuration N number of checkpoints.") + "--checkpoints", type=int, metavar="N", + default=flag_default("rollback_checkpoints"), + help="Revert configuration N number of checkpoints.") helpful.add("plugins", - "--init", action="store_true", help="Initialize plugins.") + "--init", action="store_true", help="Initialize plugins.") helpful.add("plugins", - "--prepare", action="store_true", help="Initialize and prepare plugins.") + "--prepare", action="store_true", help="Initialize and prepare plugins.") helpful.add("plugins", - "--authenticators", action="append_const", dest="ifaces", - const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.") + "--authenticators", action="append_const", dest="ifaces", + const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.") helpful.add("plugins", - "--installers", action="append_const", dest="ifaces", - const=interfaces.IInstaller, help="Limit to installer plugins only.") + "--installers", action="append_const", dest="ifaces", + const=interfaces.IInstaller, help="Limit to installer plugins only.") def _paths_parser(helpful): diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 0a92aba62..d0fae370d 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -57,7 +57,6 @@ class CLITest(unittest.TestCase): ret = cli.main(args) return ret, None, stderr, client - def test_no_flags(self): with mock.patch('letsencrypt.cli.run') as mock_run: self._call([]) @@ -91,7 +90,6 @@ class CLITest(unittest.TestCase): from letsencrypt import cli self.assertTrue(cli.USAGE in out) - def test_rollback(self): _, _, _, client = self._call(['rollback']) self.assertEqual(1, client.rollback.call_count) From 9a59a41b049482ef4c1fafe467039ddc3d31e4f9 Mon Sep 17 00:00:00 2001 From: Martijn Braam Date: Mon, 5 Oct 2015 15:08:43 +0200 Subject: [PATCH 127/145] Added bootstrap script for archlinux --- bootstrap/archlinux.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100755 bootstrap/archlinux.sh diff --git a/bootstrap/archlinux.sh b/bootstrap/archlinux.sh new file mode 100755 index 000000000..fbe0987fe --- /dev/null +++ b/bootstrap/archlinux.sh @@ -0,0 +1,2 @@ +#!/bin/sh +pacman -S git python2 python2-virtualenv gcc dialog augeas openssl libffi ca-certificates \ No newline at end of file From a86ea53a795f2425b9e682b1dbba0bc44ef8da2b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 5 Oct 2015 12:09:35 -0700 Subject: [PATCH 128/145] Added unit tests --- letsencrypt/tests/renewer_test.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index b7b2843d5..6f115abf9 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -407,6 +407,14 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.should_autodeploy(), result) self.assertEqual(self.test_rc.should_autorenew(), result) + def test_autodeployment_is_enabled(self): + self.assertTrue(self.test_rc.autodeployment_is_enabled()) + self.test_rc.configuration["autodeploy"] = "1" + self.assertTrue(self.test_rc.autodeployment_is_enabled()) + + self.test_rc.configuration["autodeploy"] = "0" + self.assertFalse(self.test_rc.autodeployment_is_enabled()) + def test_should_autodeploy(self): """Test should_autodeploy() on the basis of reasons other than expiry time window.""" @@ -427,6 +435,14 @@ class RenewableCertTests(BaseRenewableCertTest): f.write(kind) self.assertFalse(self.test_rc.should_autodeploy()) + def test_autorenewal_is_enabled(self): + self.assertTrue(self.test_rc.autorenewal_is_enabled()) + self.test_rc.configuration["autorenew"] = "1" + self.assertTrue(self.test_rc.autorenewal_is_enabled()) + + self.test_rc.configuration["autorenew"] = "0" + self.assertFalse(self.test_rc.autorenewal_is_enabled()) + @mock.patch("letsencrypt.storage.RenewableCert.ocsp_revoked") def test_should_autorenew(self, mock_ocsp): """Test should_autorenew on the basis of reasons other than From 1e18351041f330badd2db89a5b499ef4b03cbe5d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 5 Oct 2015 19:44:35 +0000 Subject: [PATCH 129/145] Fix #903: docs version parsing --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 2b4b2cd43..e2b360a6e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,7 @@ here = os.path.abspath(os.path.dirname(__file__)) # read version number (and other metadata) from package init init_fn = os.path.join(here, '..', 'letsencrypt', '__init__.py') with codecs.open(init_fn, encoding='utf8') as fd: - meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", fd.read())) + meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", fd.read())) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the From 0d880e334d3423787fb68a1fccf954750ac982a2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 5 Oct 2015 20:06:48 +0000 Subject: [PATCH 130/145] separate pip install -U setuptools pip https://travis-ci.org/jsha/boulder/jobs/83762761#L557 Success: virtualenv --no-site-packages -p python2 ./venv ./venv/bin/pip install -U setuptools pip Downloading/unpacking distribute from https://pypi.python.org/packages/source/d/distribute/distribute-0.7.3.zip#md5=c6c59594a7b180af57af8a0cc0cf5b4a Downloading distribute-0.7.3.zip (145Kb): 145Kb downloaded Running setup.py egg_info for package distribute Downloading/unpacking pip from https://pypi.python.org/packages/source/p/pip/pip-7.1.2.tar.gz#md5=3823d2343d9f3aaab21cf9c917710196 Downloading pip-7.1.2.tar.gz (1.0Mb): 1.0Mb downloaded Running setup.py egg_info for package pip warning: no previously-included files found matching '.coveragerc' warning: no previously-included files found matching '.mailmap' warning: no previously-included files found matching '.travis.yml' warning: no previously-included files found matching 'pip/_vendor/Makefile' warning: no previously-included files found matching 'tox.ini' warning: no previously-included files found matching 'dev-requirements.txt' no previously-included directories found matching '.travis' no previously-included directories found matching 'docs/_build' no previously-included directories found matching 'contrib' no previously-included directories found matching 'tasks' no previously-included directories found matching 'tests' Downloading/unpacking setuptools>=0.7 (from distribute) Downloading setuptools-18.3.2.tar.gz (626Kb): 626Kb downloaded Running setup.py egg_info for package setuptools Installing collected packages: distribute, pip, setuptools Found existing installation: distribute 0.6.24 Uninstalling distribute: Successfully uninstalled distribute Running setup.py install for distribute Found existing installation: pip 1.1 Uninstalling pip: Successfully uninstalled pip Running setup.py install for pip Traceback (most recent call last): File "", line 1, in ImportError: No module named setuptools Complete output from command /home/travis/letsencrypt/venv/bin/python2 -c "import setuptools;__file__='/home/travis/letsencrypt/venv/build/pip/setup.py';exec(compile(open(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --single-version-externally-managed --record /tmp/pip-4lZMdG-record/install-record.txt --install-headers /home/travis/letsencrypt/venv/include/site/python2.7: Traceback (most recent call last): File "", line 1, in ImportError: No module named setuptools ---------------------------------------- Rolling back uninstall of pip Exception: Traceback (most recent call last): File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/basecommand.py", line 104, in main status = self.run(options, args) File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/commands/install.py", line 250, in run requirement_set.install(install_options, global_options) File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 1137, in install requirement.rollback_uninstall() File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 491, in rollback_uninstall self.uninstalled.rollback() File "/home/travis/letsencrypt/venv/local/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 1450, in rollback pth.rollback() AttributeError: 'str' object has no attribute 'rollback' Storing complete log in /home/travis/.pip/pip.log [!] FAILURE: ./venv/bin/pip install -U setuptools pip ./venv/bin/pip install -r requirements.txt -e acme -e . -e letsencrypt-apache -e letsencrypt-nginx Traceback (most recent call last): File "./venv/bin/pip", line 5, in from pkg_resources import load_entry_point ImportError: No module named pkg_resources --- docs/using.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 16a551c1b..803a38705 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -126,7 +126,8 @@ Installation .. code-block:: shell virtualenv --no-site-packages -p python2 venv - ./venv/bin/pip install -U setuptools pip + ./venv/bin/pip install -U setuptools + ./venv/bin/pip install -U pip ./venv/bin/pip install -r requirements.txt acme/ . letsencrypt-apache/ letsencrypt-nginx/ .. warning:: Please do **not** use ``python setup.py install``. Please From eec5542cb3a479f57f6f69c407aa9aaae287e247 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 5 Oct 2015 21:39:34 +0000 Subject: [PATCH 131/145] lint archlinux bootstrap script --- bootstrap/archlinux.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bootstrap/archlinux.sh b/bootstrap/archlinux.sh index fbe0987fe..fb54c83d5 100755 --- a/bootstrap/archlinux.sh +++ b/bootstrap/archlinux.sh @@ -1,2 +1,11 @@ #!/bin/sh -pacman -S git python2 python2-virtualenv gcc dialog augeas openssl libffi ca-certificates \ No newline at end of file +pacman -S \ + git \ + python2 \ + python2-virtualenv \ + gcc \ + dialog \ + augeas \ + openssl \ + libffi \ + ca-certificates \ From 4cd5a8e42c4a31ab7f8520de3e3759ee64aff590 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 5 Oct 2015 21:40:55 +0000 Subject: [PATCH 132/145] Archlinux bootstrap: python-virtualenv --- bootstrap/archlinux.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bootstrap/archlinux.sh b/bootstrap/archlinux.sh index fb54c83d5..6de7c23d4 100755 --- a/bootstrap/archlinux.sh +++ b/bootstrap/archlinux.sh @@ -1,8 +1,12 @@ #!/bin/sh + +# "python-virtualenv" is Python3, but "python2-virtualenv" provides +# only "virtualenv2" binary, not "virtualenv" necessary in +# ./bootstrap/dev/_common_venv.sh pacman -S \ git \ python2 \ - python2-virtualenv \ + python-virtualenv \ gcc \ dialog \ augeas \ From 26e03dbba29beb32d8c64844f7ffb32a383706e7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 5 Oct 2015 22:12:21 +0000 Subject: [PATCH 133/145] docs: remove venv3. --- docs/contributing.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 9629ddcc5..3959ccee1 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -21,10 +21,8 @@ Start by :ref:`installing Let's Encrypt prerequisites .. code-block:: shell ./bootstrap/dev/venv.sh - ./bootstrap/dev/venv3.sh -Both of the commands suggest to activate the virtualenv (you can -activate one at a time only): +Activate the virtualenv: .. code-block:: shell From b275df13d3578cacfc8c199b7e2d54444d0298fa Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 6 Oct 2015 10:49:33 -0700 Subject: [PATCH 134/145] python2 and clarified coverage --- bootstrap/dev/venv.sh | 2 ++ docs/contributing.rst | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bootstrap/dev/venv.sh b/bootstrap/dev/venv.sh index 90088ac9b..d6cf95bb5 100755 --- a/bootstrap/dev/venv.sh +++ b/bootstrap/dev/venv.sh @@ -1,6 +1,8 @@ #!/bin/sh -xe # Developer virtualenv setup for Let's Encrypt client +export VENV_ARGS="--python python2" + ./bootstrap/dev/_venv_common.sh \ -r requirements.txt \ -e acme[testing] \ diff --git a/docs/contributing.rst b/docs/contributing.rst index 3959ccee1..3277d321a 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -7,9 +7,9 @@ Contributing Hacking ======= -The code base, including your pull requests, **must** have 100% unit -test coverage, pass our `integration`_ tests **and** be compliant with -the :ref:`coding style `. +All changes in your pull request **must** have 100% unit test coverage, pass +our `integration`_ tests, **and** be compliant with the +:ref:`coding style `. Bootstrap From f7241af5cee6c395104dada8d0ce9c718520a522 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 6 Oct 2015 18:58:06 +0000 Subject: [PATCH 135/145] Bump core coverage to 98% --- tox.cover.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.cover.sh b/tox.cover.sh index edfd9b81a..8418de9a8 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -16,7 +16,7 @@ fi cover () { if [ "$1" = "letsencrypt" ]; then - min=97 + min=98 elif [ "$1" = "acme" ]; then min=100 elif [ "$1" = "letsencrypt_apache" ]; then From ae66253ddfe621d3b6ef9da005373d6a67453e5f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 6 Oct 2015 19:07:20 +0000 Subject: [PATCH 136/145] Don't save KGS in dist dir in dev release script (fixes #908). --- tools/dev-release.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/dev-release.sh b/tools/dev-release.sh index 06f49f0a5..d93a6d21f 100755 --- a/tools/dev-release.sh +++ b/tools/dev-release.sh @@ -70,7 +70,7 @@ echo "Testing packages" cd "dist.$version" # start local PyPI python -m SimpleHTTPServer $PORT & -# cd .. is NOT done on purpose: we make sure that all subpacakges are +# cd .. is NOT done on purpose: we make sure that all subpackages are # installed from local PyPI rather than current directory (repo root) virtualenv --no-site-packages ../venv . ../venv/bin/activate @@ -82,15 +82,16 @@ pip install \ # stop local PyPI kill $! -# freeze before installing anythin else, so that we know end-user KGS -mkdir kgs -kgs="kgs/$version" +# freeze before installing anything else, so that we know end-user KGS +# make sure "twine upload" doesn't catch "kgs" +mkdir ../kgs +kgs="../kgs/$version" pip freeze | tee $kgs pip install nose # TODO: letsencrypt_apache fails due to symlink, c.f. #838 nosetests letsencrypt $SUBPKGS || true echo "New root: $root" -echo "KGS is at $root/$kgs" +echo "KGS is at $root/kgs" echo "In order to upload packages run the following command:" echo twine upload "$root/dist.$version/*/*" From 7e1b7ff7ae0d6f8b187b1ff19e5a94c04cb72bfd Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 6 Oct 2015 20:56:03 +0000 Subject: [PATCH 137/145] Add naive JWK Thumbprint implementation --- acme/acme/jose/jwk.py | 31 +++++++++++++++++++++++++++++++ acme/acme/jose/jwk_test.py | 23 +++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 7a976f189..2b8fa0a34 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -1,10 +1,12 @@ """JSON Web Key.""" import abc import binascii +import json import logging import cryptography.exceptions from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import rsa @@ -27,6 +29,32 @@ class JWK(json_util.TypedJSONObjectWithFields): cryptography_key_types = () """Subclasses should override.""" + required = NotImplemented + """Required members of public key's representation as defined by JWK/JWA.""" + + _thumbprint_json_dumps_params = { + # "no whitespace or line breaks before or after any syntactic + # elements" + 'indent': 0, + 'separators': (',', ':'), + # "members ordered lexicographically by the Unicode [UNICODE] + # code points of the member names" + 'sort_keys': True, + } + + def thumbprint(self, hash_function=hashes.SHA256): + """Compute JWK Thumbprint. + + https://tools.ietf.org/html/rfc7638 + + """ + digest = hashes.Hash(hash_function(), backend=default_backend()) + digest.update(json.dumps( + dict((k, v) for k, v in six.iteritems(self.to_json()) + if k in self.required), + **self._thumbprint_json_dumps_params).encode()) + return digest.finalize() + @abc.abstractmethod def public_key(self): # pragma: no cover """Generate JWK with public key. @@ -105,6 +133,7 @@ class JWKES(JWK): # pragma: no cover typ = 'ES' cryptography_key_types = ( ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) + required = ('crv', JWK.type_field_name, 'x', 'y') def fields_to_partial_json(self): raise NotImplementedError() @@ -122,6 +151,7 @@ class JWKOct(JWK): """Symmetric JWK.""" typ = 'oct' __slots__ = ('key',) + required = ('k', JWK.type_field_name) def fields_to_partial_json(self): # TODO: An "alg" member SHOULD also be present to identify the @@ -150,6 +180,7 @@ class JWKRSA(JWK): typ = 'RSA' cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey) __slots__ = ('key',) + required = ('e', JWK.type_field_name, 'n') def __init__(self, *args, **kwargs): if 'key' in kwargs and not isinstance( diff --git a/acme/acme/jose/jwk_test.py b/acme/acme/jose/jwk_test.py index 5462af6b0..d8a7410e8 100644 --- a/acme/acme/jose/jwk_test.py +++ b/acme/acme/jose/jwk_test.py @@ -25,9 +25,24 @@ class JWKTest(unittest.TestCase): self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM) -class JWKOctTest(unittest.TestCase): +class JWKTestBaseMixin(object): + """Mixin test for JWK subclass tests.""" + + thumbprint = NotImplemented + + def test_thumbprint_private(self): + self.assertEqual(self.thumbprint, self.jwk.thumbprint()) + + def test_thumbprint_public(self): + self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint()) + + +class JWKOctTest(unittest.TestCase, JWKTestBaseMixin): """Tests for acme.jose.jwk.JWKOct.""" + thumbprint = (b"=,\xdd;I\x1a+i\x02x\x8a\x12?06IM\xc2\x80" + b"\xe4\xc3\x1a\xfc\x89\xf3)'\xce\xccm\xfd5") + def setUp(self): from acme.jose.jwk import JWKOct self.jwk = JWKOct(key=b'foo') @@ -52,10 +67,13 @@ class JWKOctTest(unittest.TestCase): self.assertTrue(self.jwk.public_key() is self.jwk) -class JWKRSATest(unittest.TestCase): +class JWKRSATest(unittest.TestCase, JWKTestBaseMixin): """Tests for acme.jose.jwk.JWKRSA.""" # pylint: disable=too-many-instance-attributes + thumbprint = (b'\x08\xfa1\x87\x1d\x9b6H/*\x1eW\xc2\xe3\xf6P' + b'\xefs\x0cKB\x87\xcf\x85yO\x045\x0e\x91\x80\x0b') + def setUp(self): from acme.jose.jwk import JWKRSA self.jwk256 = JWKRSA(key=RSA256_KEY.public_key()) @@ -87,6 +105,7 @@ class JWKRSATest(unittest.TestCase): 'dq': 'bHh2u7etM8LKKCF2pY2UdQ', 'qi': 'oi45cEkbVoJjAbnQpFY87Q', }) + self.jwk = self.private def test_init_auto_comparable(self): self.assertTrue(isinstance( From 3dac62f20e8dbfacea32df6b1748d5f560b559e3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 6 Oct 2015 20:56:31 +0000 Subject: [PATCH 138/145] json_dumps_pretty: prettier separators. --- acme/acme/jose/interfaces.py | 2 +- acme/acme/jose/interfaces_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/acme/jose/interfaces.py b/acme/acme/jose/interfaces.py index f841848b3..f85777a30 100644 --- a/acme/acme/jose/interfaces.py +++ b/acme/acme/jose/interfaces.py @@ -194,7 +194,7 @@ class JSONDeSerializable(object): :rtype: str """ - return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': ')) + return self.json_dumps(sort_keys=True, indent=4) @classmethod def json_dump_default(cls, python_object): diff --git a/acme/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py index 380c3a2a5..91e6f4416 100644 --- a/acme/acme/jose/interfaces_test.py +++ b/acme/acme/jose/interfaces_test.py @@ -91,7 +91,7 @@ class JSONDeSerializableTest(unittest.TestCase): def test_json_dumps_pretty(self): self.assertEqual( - self.seq.json_dumps_pretty(), '[\n "foo1",\n "foo2"\n]') + self.seq.json_dumps_pretty(), '[\n "foo1", \n "foo2"\n]') def test_json_dump_default(self): from acme.jose.interfaces import JSONDeSerializable From c6ebfae15e91a232648e41c4a50ea93d01a15d19 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 6 Oct 2015 20:57:33 +0000 Subject: [PATCH 139/145] Unify quotes --- acme/acme/jose/jwk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 2b8fa0a34..74fa72319 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -88,7 +88,7 @@ class JWK(json_util.TypedJSONObjectWithFields): exceptions[loader] = error # no luck - raise errors.Error("Unable to deserialize key: {0}".format(exceptions)) + raise errors.Error('Unable to deserialize key: {0}'.format(exceptions)) @classmethod def load(cls, data, password=None, backend=None): @@ -109,17 +109,17 @@ class JWK(json_util.TypedJSONObjectWithFields): try: key = cls._load_cryptography_key(data, password, backend) except errors.Error as error: - logger.debug("Loading symmetric key, assymentric failed: %s", error) + logger.debug('Loading symmetric key, assymentric failed: %s', error) return JWKOct(key=data) if cls.typ is not NotImplemented and not isinstance( key, cls.cryptography_key_types): - raise errors.Error("Unable to deserialize {0} into {1}".format( + raise errors.Error('Unable to deserialize {0} into {1}'.format( key.__class__, cls.__class__)) for jwk_cls in six.itervalues(cls.TYPES): if isinstance(key, jwk_cls.cryptography_key_types): return jwk_cls(key=key) - raise errors.Error("Unsupported algorithm: {0}".format(key.__class__)) + raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__)) @JWK.register @@ -235,7 +235,7 @@ class JWKRSA(JWK): jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi')) if tuple(param for param in all_params if param is None): raise errors.Error( - "Some private parameters are missing: {0}".format( + 'Some private parameters are missing: {0}'.format( all_params)) p, q, dp, dq, qi = tuple( cls._decode_param(x) for x in all_params) From 0d89fa6d88e02257da88403898a35ffcec225947 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 7 Oct 2015 06:21:49 +0000 Subject: [PATCH 140/145] Remove SimpleHTTP TLS from Manual Plugin. --- letsencrypt/plugins/manual.py | 34 +++++------------------------- letsencrypt/plugins/manual_test.py | 8 +++---- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 3f7276725..9d5ef87e9 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -53,7 +53,7 @@ command on the target server (as root): # served and makes it more obvious that Python command will serve # anything recursively under the cwd - HTTP_TEMPLATE = """\ + CMD_TEMPLATE = """\ mkdir -p {root}/public_html/{response.URI_ROOT_PATH} cd {root}/public_html echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token} @@ -63,33 +63,10 @@ $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\ s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ s.serve_forever()" """ - """Non-TLS command template.""" - - # https://www.piware.de/2011/01/creating-an-https-server-in-python/ - HTTPS_TEMPLATE = """\ -mkdir -p {root}/public_html/{response.URI_ROOT_PATH} -cd {root}/public_html -echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token} -# run only once per server: -openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout ../key.pem -out ../cert.pem -$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ -"import BaseHTTPServer, SimpleHTTPServer, ssl; \\ -SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\ -s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ -s.socket = ssl.wrap_socket(s.socket, keyfile='../key.pem', certfile='../cert.pem'); \\ -s.serve_forever()" """ - """TLS command template. - - According to the ACME specification, "the ACME server MUST ignore - the certificate provided by the HTTPS server", so the first command - generates temporary self-signed certificate. - - """ + """Command template.""" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls - else self.HTTPS_TEMPLATE) self._root = (tempfile.mkdtemp() if self.conf("test-mode") else "/tmp/letsencrypt") self._httpd = None @@ -97,8 +74,7 @@ s.serve_forever()" """ @classmethod def add_parser_arguments(cls, add): add("test-mode", action="store_true", - help="Test mode. Executes the manual command in subprocess. " - "Requires openssl to be installed unless --no-simple-http-tls.") + help="Test mode. Executes the manual command in subprocess.") def prepare(self): # pylint: disable=missing-docstring,no-self-use pass # pragma: no cover @@ -142,11 +118,11 @@ binary for temporary key/certificate generation.""".replace("\n", "") # users, but will not work if multiple domains point at the # same server: default command doesn't support virtual hosts response, validation = achall.gen_response_and_validation( - tls=(not self.config.no_simple_http_tls)) + tls=False) # SimpleHTTP TLS is dead: ietf-wg-acme/acme#7 port = (response.port if self.config.simple_http_port is None else int(self.config.simple_http_port)) - command = self.template.format( + command = self.CMD_TEMPLATE.format( root=self._root, achall=achall, response=response, validation=pipes.quote(validation.json_dumps()), encoded_token=achall.chall.encode("token"), diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index 78bc4ae0e..8cfff1cc5 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -23,15 +23,13 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.manual import Authenticator self.config = mock.MagicMock( - no_simple_http_tls=True, simple_http_port=4430, - manual_test_mode=False) + simple_http_port=8080, manual_test_mode=False) self.auth = Authenticator(config=self.config, name="manual") self.achalls = [achallenges.SimpleHTTP( challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( - no_simple_http_tls=True, simple_http_port=4430, - manual_test_mode=True) + simple_http_port=8080, manual_test_mode=True) self.auth_test_mode = Authenticator( config=config_test_mode, name="manual") @@ -55,7 +53,7 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual([resp], self.auth.perform(self.achalls)) self.assertEqual(1, mock_raw_input.call_count) mock_verify.assert_called_with( - self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 4430) + self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 8080) message = mock_stdout.write.mock_calls[0][1][0] self.assertTrue(self.achalls[0].chall.encode("token") in message) From 73ee63779c94db8bb270415fb209e016cee7c2c4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 7 Oct 2015 06:23:28 +0000 Subject: [PATCH 141/145] Remove --no-simple-http-tls --- letsencrypt/cli.py | 2 -- letsencrypt/interfaces.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 0bd5f537e..64cba508d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -702,8 +702,6 @@ def create_parser(plugins, args): help=config_help("dvsni_port")) helpful.add("testing", "--simple-http-port", type=int, help=config_help("simple_http_port")) - helpful.add("testing", "--no-simple-http-tls", action="store_true", - help=config_help("no_simple_http_tls")) helpful.add_group( "security", description="Security parameters & server settings") diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 1f51645ab..5e82d61aa 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -223,8 +223,6 @@ class IConfig(zope.interface.Interface): "Port number to perform DVSNI challenge. " "Boulder in testing mode defaults to 5001.") - no_simple_http_tls = zope.interface.Attribute( - "Do not use TLS when solving SimpleHTTP challenges.") simple_http_port = zope.interface.Attribute( "Port used in the SimpleHttp challenge.") From e4e94b20d44dde81e1a9f0a532ef447f04da8c48 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 7 Oct 2015 06:23:40 +0000 Subject: [PATCH 142/145] Remove --no-simple-http-tls from integration tests --- tests/boulder-integration.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index ed877d136..25db8ba6d 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -24,7 +24,6 @@ common() { common --domains le1.wtf auth common --domains le2.wtf run common -a manual -d le.wtf auth -common -a manual -d le.wtf --no-simple-http-tls auth export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ OPENSSL_CNF=examples/openssl.cnf From 0034a8fae45131e46a27128575eff02f51f79f2c Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 7 Oct 2015 22:40:02 +0000 Subject: [PATCH 143/145] Add docs to tarballs (fixes #884). --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 80fd8777e..e421e0cd7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,4 +5,5 @@ include CONTRIBUTING.md include LICENSE.txt include linter_plugin.py include letsencrypt/EULA +recursive-include docs * recursive-include letsencrypt/tests/testdata * From 9e1477faa48148ad5b2c3664375a3520d589eb94 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 8 Oct 2015 19:28:55 +0000 Subject: [PATCH 144/145] Release 0.0.0.dev20151008 --- acme/setup.py | 2 +- letsencrypt-apache/setup.py | 2 +- letsencrypt-nginx/setup.py | 2 +- letsencrypt/__init__.py | 2 +- letshelp-letsencrypt/setup.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 6448b7fe9..36a724f97 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.1.0.dev0' +version = '0.0.0.dev20151008' install_requires = [ # load_pem_private/public_key (>=0.6) diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 626e700b2..ef9299b2c 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.1.0.dev0' +version = '0.0.0.dev20151008' install_requires = [ 'acme=={0}'.format(version), diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index a37b8222b..dde7243a9 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.1.0.dev0' +version = '0.0.0.dev20151008' install_requires = [ 'acme=={0}'.format(version), diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 1155a5b0c..d7ec9c5e3 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,4 +1,4 @@ """Let's Encrypt client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.1.0.dev0' +__version__ = '0.0.0.dev20151008' diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index a83fc8843..ad517c74e 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.1.0.dev0' +version = '0.0.0.dev20151008' install_requires = [ 'setuptools', # pkg_resources From 6dfb75b96f6e5126e590b38e608ebedb06c7b2bc Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 8 Oct 2015 20:32:15 +0000 Subject: [PATCH 145/145] Fix #928 test_json_dumps_pretty py3 compat. --- acme/acme/jose/interfaces_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/acme/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py index 91e6f4416..84dc2a1be 100644 --- a/acme/acme/jose/interfaces_test.py +++ b/acme/acme/jose/interfaces_test.py @@ -1,6 +1,8 @@ """Tests for acme.jose.interfaces.""" import unittest +import six + class JSONDeSerializableTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes @@ -90,8 +92,9 @@ class JSONDeSerializableTest(unittest.TestCase): self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps()) def test_json_dumps_pretty(self): - self.assertEqual( - self.seq.json_dumps_pretty(), '[\n "foo1", \n "foo2"\n]') + filler = ' ' if six.PY2 else '' + self.assertEqual(self.seq.json_dumps_pretty(), + '[\n "foo1",{0}\n "foo2"\n]'.format(filler)) def test_json_dump_default(self): from acme.jose.interfaces import JSONDeSerializable