From ae6c7cb9363fa97d30d000516782968c82bdfea0 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 22 May 2015 07:28:21 +0000 Subject: [PATCH 001/109] --le-vhost-ext -> --apache-le-vhost-ext --- letsencrypt/cli.py | 4 +--- letsencrypt_apache/configurator.py | 10 +++++++--- letsencrypt_apache/constants.py | 1 + letsencrypt_apache/tests/util.py | 2 +- letsencrypt_nginx/tests/util.py | 3 +-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index defa7633d..ecf6764f7 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -318,13 +318,11 @@ def _paths_parser(parser): help=config_help("work_dir")) add("--backup-dir", default=flag_default("backup_dir"), help=config_help("backup_dir")) + add("--key-dir", default=flag_default("key_dir"), help=config_help("key_dir")) add("--cert-dir", default=flag_default("certs_dir"), help=config_help("cert_dir")) - - add("--le-vhost-ext", default="-le-ssl.conf", - help=config_help("le_vhost_ext")) add("--cert-path", default=flag_default("cert_path"), help=config_help("cert_path")) add("--chain-path", default=flag_default("chain_path"), diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 102718e13..24ccb0859 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -97,6 +97,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): add("init-script", default=constants.CLI_DEFAULTS["init_script"], help="Path to the Apache init script (used for server " "reload/restart).") + add("le-vhost-ext", default=constants.CLI_DEFAULTS["le_vhost_ext"], + help="SSL vhost configuration extension.") + def __init__(self, *args, **kwargs): """Initialize an Apache Configurator. @@ -448,7 +451,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Makes an ssl_vhost version of a nonssl_vhost. Duplicates vhost and adds default ssl options - New vhost will reside as (nonssl_vhost.path) + ``IConfig.le_vhost_ext`` + New vhost will reside as (nonssl_vhost.path) + + ``letsencrypt_apache.constants.CLI_DEFAULTS["le_vhost_ext"]`` .. note:: This function saves the configuration @@ -462,9 +466,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): avail_fp = nonssl_vhost.filep # Get filepath of new ssl_vhost if avail_fp.endswith(".conf"): - ssl_fp = avail_fp[:-(len(".conf"))] + self.config.le_vhost_ext + ssl_fp = avail_fp[:-(len(".conf"))] + self.conf("le_vhost_ext") else: - ssl_fp = avail_fp + self.config.le_vhost_ext + ssl_fp = avail_fp + self.conf("le_vhost_ext") # First register the creation so that it is properly removed if # configuration is rolled back diff --git a/letsencrypt_apache/constants.py b/letsencrypt_apache/constants.py index b40e2ac65..865741823 100644 --- a/letsencrypt_apache/constants.py +++ b/letsencrypt_apache/constants.py @@ -8,6 +8,7 @@ CLI_DEFAULTS = dict( ctl="apache2ctl", enmod="a2enmod", init_script="/etc/init.d/apache2", + le_vhost_ext="-le-ssl.conf", ) """CLI defaults.""" diff --git a/letsencrypt_apache/tests/util.py b/letsencrypt_apache/tests/util.py index fc2dcfc79..e637b0890 100644 --- a/letsencrypt_apache/tests/util.py +++ b/letsencrypt_apache/tests/util.py @@ -75,7 +75,7 @@ def get_apache_configurator( config=mock.MagicMock( apache_server_root=config_path, apache_mod_ssl_conf=ssl_options, - le_vhost_ext="-le-ssl.conf", + 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"), diff --git a/letsencrypt_nginx/tests/util.py b/letsencrypt_nginx/tests/util.py index c53250f1b..45d1fa184 100644 --- a/letsencrypt_nginx/tests/util.py +++ b/letsencrypt_nginx/tests/util.py @@ -46,8 +46,7 @@ def get_nginx_configurator( config = configurator.NginxConfigurator( config=mock.MagicMock( nginx_server_root=config_path, nginx_mod_ssl_conf=ssl_options, - le_vhost_ext="-le-ssl.conf", backup_dir=backups, - config_dir=config_dir, work_dir=work_dir, + backup_dir=backups, config_dir=config_dir, work_dir=work_dir, temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), in_progress_dir=os.path.join(backups, "IN_PROGRESS")), name="nginx", From f00b674131aab1fa86c50027b65c6665e3f4003a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 22 May 2015 07:55:22 +0000 Subject: [PATCH 002/109] Move --cert-path and --chain-path from global IConfig to subparsers. --- letsencrypt/cli.py | 52 +++++++++++++++++++++++++-------------- letsencrypt/client.py | 17 +++++++------ letsencrypt/constants.py | 18 ++++++++------ letsencrypt/interfaces.py | 8 ++---- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index ecf6764f7..8cd8d9955 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -98,8 +98,9 @@ def run(args, config, plugins): return "Configurator could not be determined" acme, doms = _common_run(args, config, acc, authenticator, installer) - cert_key, cert_path, chain_path = acme.obtain_certificate(doms) - acme.deploy_certificate(doms, cert_key, cert_path, chain_path) + cert_key, act_cert_path, act_chain_path = acme.obtain_certificate( + doms, args.cert_path, args.chain_path) + acme.deploy_certificate(doms, cert_key, act_cert_path, act_chain_path) acme.enhance_config(doms, args.redirect) @@ -121,7 +122,7 @@ def auth(args, config, plugins): acme, doms = _common_run( args, config, acc, authenticator=authenticator, installer=installer) - acme.obtain_certificate(doms) + acme.obtain_certificate(doms, args.cert_path, args.chain_path) def install(args, config, plugins): @@ -135,21 +136,21 @@ def install(args, config, plugins): return "Installer could not be determined" acme, doms = _common_run( args, config, acc, authenticator=None, installer=installer) - assert args.cert_path is not None + assert args.cert_path is not None # required=True in the subparser acme.deploy_certificate(doms, acc.key, args.cert_path, args.chain_path) acme.enhance_config(doms, args.redirect) def revoke(args, unused_config, unused_plugins): """Revoke.""" - if args.rev_cert is None and args.rev_key is None: - return "At least one of --certificate or --key is required" + if args.cert_path is None and args.key_path is None: + return "At least one of --cert-path or --key-path is required" # This depends on the renewal config and cannot be completed yet. zope.component.getUtility(interfaces.IDisplay).notification( "Revocation is not available with the new Boulder server yet.") #client.revoke(args.installer, config, plugins, args.no_confirm, - # args.rev_cert, args.rev_key) + # args.cert_path, args.key_path) def rollback(args, config, plugins): @@ -248,13 +249,29 @@ def create_parser(plugins): subparser.set_defaults(func=func) return subparser - add_subparser("run", run) - add_subparser("auth", auth) - add_subparser("install", install) - parser_revoke = add_subparser("revoke", revoke) - parser_rollback = add_subparser("rollback", rollback) + parser_run = add_subparser("run", run) + parser_auth = add_subparser("auth", auth) add_subparser("config_changes", config_changes) + for subparser in (parser_run, parser_auth): + subparser.add_argument( + "--cert-path", default=flag_default("cert_path"), + help="Candidate path where a freshly issued certificate will " + "be saved to. If a file already exists at the provided " + "path, dirpath/0001_filename.ext will be attempted " + "(securely).") + subparser.add_argument( + "--chain-path", default=flag_default("chain_path"), + help="Candidate path (see --cert-path help) where an " + "accompanying certificate chain will be saved.") + + parser_install = add_subparser("install", install) + parser_install.add_argument( + "--cert-path", required=True, help="Path to a certificate that " + "is going to be installed.") + parser_install.add_argument( + "--chain-path", help="Accompanying path to a certificate chain.") + parser_plugins = add_subparser("plugins", plugins_cmd) parser_plugins.add_argument("--init", action="store_true") parser_plugins.add_argument("--prepare", action="store_true") @@ -287,13 +304,14 @@ def create_parser(plugins): help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") + parser_revoke = add_subparser("revoke", revoke) parser_revoke.add_argument( - "--certificate", dest="rev_cert", type=read_file, metavar="CERT_PATH", - help="Revoke a specific certificate.") + "--cert-path", type=read_file, help="Revoke a specific certificate.") parser_revoke.add_argument( - "--key", dest="rev_key", type=read_file, metavar="KEY_PATH", + "--key-path", type=read_file, help="Revoke all certs generated by the provided authorized key.") + parser_rollback = add_subparser("rollback", rollback) parser_rollback.add_argument( "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), @@ -323,10 +341,6 @@ def _paths_parser(parser): help=config_help("key_dir")) add("--cert-dir", default=flag_default("certs_dir"), help=config_help("cert_dir")) - add("--cert-path", default=flag_default("cert_path"), - help=config_help("cert_path")) - add("--chain-path", default=flag_default("chain_path"), - help=config_help("chain_path")) return parser diff --git a/letsencrypt/client.py b/letsencrypt/client.py index ae1667dfa..4362592f8 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -95,7 +95,7 @@ class Client(object): self.account.save() - def obtain_certificate(self, domains, csr=None): + def obtain_certificate(self, domains, cert_path, chain_path, csr=None): """Obtains a certificate from the ACME server. :meth:`.register` must be called before :meth:`.obtain_certificate` @@ -104,6 +104,9 @@ class Client(object): :param set domains: domains to get a certificate + :param str cert_path: Candidate path to a certificate. + :param str chain_path: Candidate path to a certificate chain. + :param csr: CSR must contain requested domains, the key used to generate this CSR can be different than self.authkey :type csr: :class:`CSR` @@ -137,13 +140,13 @@ class Client(object): authzr) # Save Certificate - cert_path, chain_path = self.save_certificate( - certr, self.config.cert_path, self.config.chain_path) + act_cert_path, act_chain_path = self.save_certificate( + certr, cert_path, chain_path) revoker.Revoker.store_cert_key( - cert_path, self.account.key.file, self.config) + act_cert_path, self.account.key.file, self.config) - return cert_key, cert_path, chain_path + return cert_key, act_cert_path, act_chain_path def save_certificate(self, certr, cert_path, chain_path): # pylint: disable=no-self-use @@ -152,8 +155,8 @@ class Client(object): :param certr: ACME "certificate" resource. :type certr: :class:`acme.messages.Certificate` - :param str cert_path: Path to attempt to save the cert file - :param str chain_path: Path to attempt to save the chain file + :param str cert_path: Candidate path to a certificate. + :param str chain_path: Candidate path to a certificate chain. :returns: cert_path, chain_path (absolute paths to the actual files) :rtype: `tuple` of `str` diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 9ff0b128c..29365797a 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -1,5 +1,6 @@ """Let's Encrypt constants.""" import logging +import os from acme import challenges @@ -8,19 +9,22 @@ SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins" """Setuptools entry point group name for plugins.""" +_CLI_DEFAULT_CONFIG_DIR = "/etc/letsencrypt" +_CLI_DEFAULT_WORK_DIR = "/var/lib/letsencrypt" +_CLI_DEFAULT_CERT_DIR = os.path.join(_CLI_DEFAULT_CONFIG_DIR, "certs") CLI_DEFAULTS = dict( config_files=["/etc/letsencrypt/cli.ini"], verbose_count=-(logging.WARNING / 10), server="https://www.letsencrypt-demo.org/acme/new-reg", rsa_key_size=2048, rollback_checkpoints=0, - config_dir="/etc/letsencrypt", - work_dir="/var/lib/letsencrypt", - backup_dir="/var/lib/letsencrypt/backups", - key_dir="/etc/letsencrypt/keys", - certs_dir="/etc/letsencrypt/certs", - cert_path="/etc/letsencrypt/certs/cert-letsencrypt.pem", - chain_path="/etc/letsencrypt/certs/chain-letsencrypt.pem", + config_dir=_CLI_DEFAULT_CONFIG_DIR, + work_dir=_CLI_DEFAULT_CONFIG_DIR, + backup_dir=os.path.join(_CLI_DEFAULT_WORK_DIR, "backups"), + key_dir=os.path.join(_CLI_DEFAULT_CONFIG_DIR, "keys"), + certs_dir=_CLI_DEFAULT_CERT_DIR, + cert_path=os.path.join(_CLI_DEFAULT_CERT_DIR, "cert-letsencrypt.pem"), + chain_path=os.path.join(_CLI_DEFAULT_CERT_DIR, "chain-letsencrypt.pem"), test_mode=False, ) """Defaults for CLI flags and `.IConfig` attributes.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 609b9410a..68d442227 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -170,13 +170,9 @@ class IConfig(zope.interface.Interface): "Directory where all account keys are stored.") rec_token_dir = zope.interface.Attribute( "Directory where all recovery tokens are saved.") - key_dir = zope.interface.Attribute("Keys storage.") - cert_dir = zope.interface.Attribute("Certificates storage.") - le_vhost_ext = zope.interface.Attribute( - "SSL vhost configuration extension.") - cert_path = zope.interface.Attribute("Let's Encrypt certificate file path.") - chain_path = zope.interface.Attribute("Let's Encrypt chain file path.") + key_dir = zope.interface.Attribute("Keys storage.") + cert_dir = zope.interface.Attribute("Certificates and CSRs storage.") test_mode = zope.interface.Attribute( "Test mode. Disables certificate verification.") From 53d65d005e20906e4af9da985f85d2a10f6ec6ba Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 22 May 2015 08:43:58 +0000 Subject: [PATCH 003/109] CLI: improve plugins help messages, refactor code --- letsencrypt/cli.py | 140 +++++++++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 55 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8cd8d9955..5218af06a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -242,50 +242,6 @@ def create_parser(plugins): add("--test-mode", action="store_true", help=config_help("test_mode"), default=flag_default("test_mode")) - subparsers = 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__) - subparser.set_defaults(func=func) - return subparser - - parser_run = add_subparser("run", run) - parser_auth = add_subparser("auth", auth) - add_subparser("config_changes", config_changes) - - for subparser in (parser_run, parser_auth): - subparser.add_argument( - "--cert-path", default=flag_default("cert_path"), - help="Candidate path where a freshly issued certificate will " - "be saved to. If a file already exists at the provided " - "path, dirpath/0001_filename.ext will be attempted " - "(securely).") - subparser.add_argument( - "--chain-path", default=flag_default("chain_path"), - help="Candidate path (see --cert-path help) where an " - "accompanying certificate chain will be saved.") - - parser_install = add_subparser("install", install) - parser_install.add_argument( - "--cert-path", required=True, help="Path to a certificate that " - "is going to be installed.") - parser_install.add_argument( - "--chain-path", help="Accompanying path to a certificate chain.") - - parser_plugins = add_subparser("plugins", plugins_cmd) - parser_plugins.add_argument("--init", action="store_true") - parser_plugins.add_argument("--prepare", action="store_true") - parser_plugins.add_argument( - "--authenticators", action="append_const", dest="ifaces", - const=interfaces.IAuthenticator) - parser_plugins.add_argument( - "--installers", action="append_const", dest="ifaces", - const=interfaces.IInstaller) - - parser.add_argument("--configurator") - parser.add_argument("-a", "--authenticator") - parser.add_argument("-i", "--installer") - # positional arg shadows --domains, instead of appending, and # --domains is useful, because it can be stored in config #for subparser in parser_run, parser_auth, parser_install: @@ -304,29 +260,75 @@ def create_parser(plugins): help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") + _paths_parser(parser.add_argument_group("paths")) + # _plugins_parsing should be the last thing to act upon the main + # parser (--help should display plugin-specific options last) + _plugins_parsing(parser, plugins) + + _create_subparsers(parser) + + return parser + + +def _create_subparsers(parser): + subparsers = 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__) + subparser.set_defaults(func=func) + return subparser + + # the order of add_subparser() calls is important: it defines the + # order in which subparser names will be displayed in --help + parser_run = add_subparser("run", run) + parser_auth = add_subparser("auth", auth) + parser_install = add_subparser("install", install) + parser_plugins = add_subparser("plugins", plugins_cmd) parser_revoke = add_subparser("revoke", revoke) + parser_rollback = add_subparser("rollback", rollback) + add_subparser("config_changes", config_changes) + + for subparser in (parser_run, parser_auth): + subparser.add_argument( + "--cert-path", default=flag_default("cert_path"), + help="Candidate path where a freshly issued certificate will " + "be saved to. If a file already exists at the provided " + "path, dirpath/0001_filename.ext will be attempted " + "(securely).") + subparser.add_argument( + "--chain-path", default=flag_default("chain_path"), + help="Candidate path (see --cert-path help) where an " + "accompanying certificate chain will be saved.") + + parser_install.add_argument( + "--cert-path", required=True, help="Path to a certificate that " + "is going to be installed.") + parser_install.add_argument( + "--chain-path", help="Accompanying path to a certificate chain.") + + parser_plugins.add_argument( + "--init", action="store_true", help="Initialize plugins.") + parser_plugins.add_argument("--prepare", action="store_true", + help="Initialize and prepare plugins.") + parser_plugins.add_argument( + "--authenticators", action="append_const", dest="ifaces", + const=interfaces.IAuthenticator, + help="Limit to authenticator plugins only.") + parser_plugins.add_argument( + "--installers", action="append_const", dest="ifaces", + const=interfaces.IInstaller, help="Limit to installer plugins only.") + parser_revoke.add_argument( "--cert-path", type=read_file, help="Revoke a specific certificate.") parser_revoke.add_argument( "--key-path", type=read_file, help="Revoke all certs generated by the provided authorized key.") - parser_rollback = add_subparser("rollback", rollback) parser_rollback.add_argument( "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), help="Revert configuration N number of checkpoints.") - _paths_parser(parser.add_argument_group("paths")) - - # TODO: plugin_parser should be called for every detected plugin - for name, plugin_ep in plugins.iteritems(): - plugin_ep.plugin_cls.inject_parser_options( - parser.add_argument_group( - name, description=plugin_ep.description), name) - - return parser - def _paths_parser(parser): add = parser.add_argument @@ -345,6 +347,34 @@ def _paths_parser(parser): return parser +def _plugins_parsing(parser, plugins): + plugins_group = parser.add_argument_group( + "plugins", description="Let's Encrypt client supports an extensible " + "plugins architecture. See '%(prog)s plugins' for a list of all " + "available plugins and their names. You can force a particular " + "plugin by setting options provided below. Futher down this help " + "message you will find plugin-specific options (prefixed by " + "--{plugin_name}.") + plugins_group.add_argument( + "-a", "--authenticator", help="Authenticator plugin name.") + plugins_group.add_argument( + "-i", "--installer", help="Installer plugin name.") + plugins_group.add_argument( + "--configurator", help="Name of the plugin that is both " + "an authenticator and an installer. Should not be used together " + "with --authenticator or --installer.") + + # things should not be reorder past/pre this comment: + # plugins_group should be displayed in --help before plugin + # specific groups (so that plugins_group.description makes sense) + + for name, plugin_ep in plugins.iteritems(): + plugin_ep.plugin_cls.inject_parser_options( + parser.add_argument_group( + "plugins: {0}".format(name), + description=plugin_ep.description), name) + + def main(args=sys.argv[1:]): """Command line argument parsing and main script execution.""" # note: arg parser internally handles --help (and exits afterwards) From 71aa1a5348051487844e69d5ca3a277f8a213817 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 28 May 2015 20:52:59 +0000 Subject: [PATCH 004/109] Fix merge problems and pylint --- letsencrypt/cli.py | 4 ++-- letsencrypt/client.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index f2b4f9d4f..bbff3411b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -288,8 +288,8 @@ def _create_subparsers(parser): # the order of add_subparser() calls is important: it defines the # order in which subparser names will be displayed in --help - parser_run = add_subparser("run", run) - parser_auth = add_subparser("auth", auth) + add_subparser("run", run) + add_subparser("auth", auth) parser_install = add_subparser("install", install) parser_plugins = add_subparser("plugins", plugins_cmd) parser_revoke = add_subparser("revoke", revoke) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 34a5aefdd..bd467b13d 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -100,7 +100,7 @@ class Client(object): self.account.save() - def obtain_certificate(self, domains, cert_path, chain_path, csr=None): + def obtain_certificate(self, domains, csr=None): """Obtains a certificate from the ACME server. :meth:`.register` must be called before :meth:`.obtain_certificate` @@ -109,9 +109,6 @@ class Client(object): :param set domains: domains to get a certificate - :param str cert_path: Candidate path to a certificate. - :param str chain_path: Candidate path to a certificate chain. - :param csr: CSR must contain requested domains, the key used to generate this CSR can be different than self.authkey :type csr: :class:`CSR` From 3fefd280802e8c88d949314b1e246ad31d663bed Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 1 Jun 2015 23:25:57 +0000 Subject: [PATCH 005/109] Only configure --config-dir/--work-dir (rest dynamic). --- letsencrypt/cli.py | 10 ---- letsencrypt/configuration.py | 63 ++++++++++++++++--------- letsencrypt/constants.py | 55 +++++++++++---------- letsencrypt/interfaces.py | 22 ++++----- letsencrypt/tests/configuration_test.py | 26 ++++++---- letsencrypt_apache/tests/util.py | 3 -- letsencrypt_nginx/tests/util.py | 4 +- 7 files changed, 99 insertions(+), 84 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 03367e2cb..45fe271bb 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -332,16 +332,6 @@ def _paths_parser(parser): help=config_help("config_dir")) add("--work-dir", default=flag_default("work_dir"), help=config_help("work_dir")) - add("--backup-dir", default=flag_default("backup_dir"), - help=config_help("backup_dir")) - - add("--key-dir", default=flag_default("key_dir"), - help=config_help("key_dir")) - add("--cert-dir", default=flag_default("certs_dir"), - help=config_help("cert_dir")) - - add("--renewer-config-file", default=flag_default("renewer_config_file"), - help=config_help("renewer_config_file")) return parser diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 6a808a6a9..00b45040a 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -17,10 +17,15 @@ class NamespaceConfig(object): :attr:`~letsencrypt.interfaces.IConfig.work_dir` and relative paths defined in :py:mod:`letsencrypt.constants`: - - ``temp_checkpoint_dir`` - - ``in_progress_dir`` - - ``cert_key_backup`` - - ``rec_token_dir`` + - `accounts_dir` + - `account_keys_dir` + - `cert_dir` + - `cert_key_backup` + - `in_progress_dir` + - `key_dir` + - `rec_token_dir` + - `renewer_config_file` + - `temp_checkpoint_dir` :ivar namespace: Namespace typically produced by :meth:`argparse.ArgumentParser.parse_args`. @@ -35,27 +40,12 @@ class NamespaceConfig(object): def __getattr__(self, name): return getattr(self.namespace, name) - @property - def temp_checkpoint_dir(self): # pylint: disable=missing-docstring - return os.path.join( - self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) - - @property - def in_progress_dir(self): # pylint: disable=missing-docstring - return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR) - @property def server_path(self): """File path based on ``server``.""" parsed = urlparse.urlparse(self.namespace.server) return (parsed.netloc + parsed.path).replace('/', os.path.sep) - @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 accounts_dir(self): #pylint: disable=missing-docstring return os.path.join( @@ -63,11 +53,40 @@ class NamespaceConfig(object): @property def account_keys_dir(self): #pylint: disable=missing-docstring - return os.path.join( - self.namespace.config_dir, constants.ACCOUNTS_DIR, - self.server_path, constants.ACCOUNT_KEYS_DIR) + return os.path.join(self.accounts_dir, constants.ACCOUNT_KEYS_DIR) + + @property + def backup_dir(self): # pylint: disable=missing-docstring + 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) + + @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) + + @property + def key_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.KEY_DIR) # TODO: This should probably include the server name @property def rec_token_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.REC_TOKEN_DIR) + + @property + def renewer_config_file(self): # pylint: disable=missing-docstring + return os.path.join( + self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME) + + @property + def temp_checkpoint_dir(self): # pylint: disable=missing-docstring + return os.path.join( + self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 5b7c3af29..6e2355252 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -1,6 +1,5 @@ """Let's Encrypt constants.""" import logging -import os from acme import challenges @@ -8,24 +7,14 @@ from acme import challenges SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins" """Setuptools entry point group name for plugins.""" - -_CLI_DEFAULT_CONFIG_DIR = "/etc/letsencrypt" -_CLI_DEFAULT_WORK_DIR = "/var/lib/letsencrypt" -_CLI_DEFAULT_CERT_DIR = os.path.join(_CLI_DEFAULT_CONFIG_DIR, "certs") CLI_DEFAULTS = dict( config_files=["/etc/letsencrypt/cli.ini"], verbose_count=-(logging.WARNING / 10), server="https://www.letsencrypt-demo.org/acme/new-reg", rsa_key_size=2048, rollback_checkpoints=0, - config_dir=_CLI_DEFAULT_CONFIG_DIR, - work_dir=_CLI_DEFAULT_CONFIG_DIR, - backup_dir=os.path.join(_CLI_DEFAULT_WORK_DIR, "backups"), - key_dir=os.path.join(_CLI_DEFAULT_CONFIG_DIR, "keys"), - certs_dir=_CLI_DEFAULT_CERT_DIR, - cert_path=os.path.join(_CLI_DEFAULT_CERT_DIR, "cert-letsencrypt.pem"), - chain_path=os.path.join(_CLI_DEFAULT_CERT_DIR, "chain-letsencrypt.pem"), - renewer_config_file=os.path.join(_CLI_DEFAULT_CONFIG_DIR, "renewer.conf"), + config_dir="/etc/letsencrypt", + work_dir="/var/lib/letsencrypt", test_mode=False, ) """Defaults for CLI flags and `.IConfig` attributes.""" @@ -64,26 +53,40 @@ List of expected options parameters: CONFIG_DIRS_MODE = 0o755 """Directory mode for ``.IConfig.config_dir`` et al.""" -TEMP_CHECKPOINT_DIR = "temp_checkpoint" -"""Temporary checkpoint directory (relative to IConfig.work_dir).""" - -IN_PROGRESS_DIR = "IN_PROGRESS" -"""Directory used before a permanent checkpoint is finalized (relative to -IConfig.work_dir).""" - -CERT_KEY_BACKUP_DIR = "keys-certs" -"""Directory where all certificates and keys are stored (relative to -IConfig.work_dir. Used for easy revocation.""" - ACCOUNTS_DIR = "accounts" """Directory where all accounts are saved.""" ACCOUNT_KEYS_DIR = "keys" -"""Directory where account keys are saved. Relative to ACCOUNTS_DIR.""" +"""Directory where account keys are saved. Relative to `ACCOUNTS_DIR`.""" + +BACKUP_DIR = "backups" +"""Directory (relative to `IConfig.work_dir`) where backups are kept.""" + +CERT_KEY_BACKUP_DIR = "keys-certs" +"""Directory where all certificates and keys are stored (relative to +`IConfig.work_dir`). Used for easy revocation.""" + +CERT_DIR = "certs" +"""Directory (relative to `IConfig.config_dir`) where CSRs are saved.""" + +IN_PROGRESS_DIR = "IN_PROGRESS" +"""Directory used before a permanent checkpoint is finalized (relative to +`IConfig.work_dir`).""" + +KEYS_DIR = "keys" +"""Directory (relative to `IConfig.config_dir`) where keys are saved.""" + +TEMP_CHECKPOINT_DIR = "temp_checkpoint" +"""Temporary checkpoint directory (relative to `IConfig.work_dir`).""" REC_TOKEN_DIR = "recovery_tokens" """Directory where all recovery tokens are saved (relative to -IConfig.work_dir).""" +`IConfig.work_dir`).""" + + +RENEWER_CONFIG_FILENAME = "renewer.conf" +"""Renewer config file name (relative to `IConfig.config_dir`).""" + NETSTAT = "/bin/netstat" """Location of netstat binary for checking whether a listener is already diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 365b9c182..421c10402 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -156,23 +156,23 @@ class IConfig(zope.interface.Interface): config_dir = zope.interface.Attribute("Configuration directory.") work_dir = zope.interface.Attribute("Working directory.") - backup_dir = zope.interface.Attribute("Configuration backups directory.") - temp_checkpoint_dir = zope.interface.Attribute( - "Temporary checkpoint directory.") - in_progress_dir = zope.interface.Attribute( - "Directory used before a permanent checkpoint is finalized.") - cert_key_backup = zope.interface.Attribute( - "Directory where all certificates and keys are stored. " - "Used for easy revocation.") + accounts_dir = zope.interface.Attribute( "Directory where all account information is stored.") account_keys_dir = zope.interface.Attribute( "Directory where all account keys are stored.") + backup_dir = zope.interface.Attribute("Configuration backups directory.") + cert_dir = zope.interface.Attribute("Certificates and CSRs storage.") + 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.") rec_token_dir = zope.interface.Attribute( "Directory where all recovery tokens are saved.") - - key_dir = zope.interface.Attribute("Keys storage.") - cert_dir = zope.interface.Attribute("Certificates and CSRs storage.") + temp_checkpoint_dir = zope.interface.Attribute( + "Temporary checkpoint directory.") renewer_config_file = zope.interface.Attribute( "Location of renewal configuration file.") diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index d25368feb..345e3abbc 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -30,23 +30,31 @@ class NamespaceConfigTest(unittest.TestCase): @mock.patch('letsencrypt.configuration.constants') def test_dynamic_dirs(self, constants): - constants.TEMP_CHECKPOINT_DIR = 't' - constants.IN_PROGRESS_DIR = '../p' - constants.CERT_KEY_BACKUP_DIR = 'c/' - constants.REC_TOKEN_DIR = '/r' constants.ACCOUNTS_DIR = 'acc' constants.ACCOUNT_KEYS_DIR = 'keys' + constants.BACKUP_DIR = 'backups' + constants.CERT_DIR = 'certs' + constants.CERT_KEY_BACKUP_DIR = 'c/' + constants.IN_PROGRESS_DIR = '../p' + constants.KEY_DIR = 'keys' + constants.REC_TOKEN_DIR = '/r' + constants.RENEWER_CONFIG_FILENAME = 'r.conf' + constants.TEMP_CHECKPOINT_DIR = 't' - self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') - self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') - self.assertEqual( - self.config.cert_key_backup, '/tmp/foo/c/acme-server.org:443/new') - self.assertEqual(self.config.rec_token_dir, '/r') self.assertEqual( self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new') self.assertEqual( self.config.account_keys_dir, '/tmp/config/acc/acme-server.org:443/new/keys') + 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.rec_token_dir, '/r') + self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') + self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') if __name__ == '__main__': diff --git a/letsencrypt_apache/tests/util.py b/letsencrypt_apache/tests/util.py index e637b0890..a5e700682 100644 --- a/letsencrypt_apache/tests/util.py +++ b/letsencrypt_apache/tests/util.py @@ -76,10 +76,7 @@ def get_apache_configurator( apache_server_root=config_path, apache_mod_ssl_conf=ssl_options, 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) diff --git a/letsencrypt_nginx/tests/util.py b/letsencrypt_nginx/tests/util.py index 45d1fa184..fd1418aa3 100644 --- a/letsencrypt_nginx/tests/util.py +++ b/letsencrypt_nginx/tests/util.py @@ -46,9 +46,7 @@ def get_nginx_configurator( config = configurator.NginxConfigurator( config=mock.MagicMock( nginx_server_root=config_path, nginx_mod_ssl_conf=ssl_options, - backup_dir=backups, config_dir=config_dir, work_dir=work_dir, - temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), - in_progress_dir=os.path.join(backups, "IN_PROGRESS")), + config_dir=config_dir, work_dir=work_dir), name="nginx", version=version) config.prepare() From 8fe65843361b9cd025c36632c4bb9c630448dd2c Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 00:14:10 +0000 Subject: [PATCH 006/109] Don't allow user supplied mod_ssl conf destination (fixes #451). --- letsencrypt_apache/configurator.py | 12 +++++++----- letsencrypt_apache/constants.py | 5 +++-- letsencrypt_apache/tests/util.py | 1 - letsencrypt_nginx/configurator.py | 13 +++++++------ letsencrypt_nginx/constants.py | 8 +++++--- letsencrypt_nginx/tests/util.py | 5 ++--- 6 files changed, 24 insertions(+), 20 deletions(-) diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index b85774494..965e9cf73 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -87,8 +87,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): def add_parser_arguments(cls, add): add("server-root", default=constants.CLI_DEFAULTS["server_root"], help="Apache server root directory.") - add("mod-ssl-conf", default=constants.CLI_DEFAULTS["mod_ssl_conf"], - help="Contains standard Apache SSL directives.") add("ctl", default=constants.CLI_DEFAULTS["ctl"], help="Path to the 'apache2ctl' binary, used for 'configtest' and " "retrieving Apache2 version number.") @@ -126,10 +124,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.vhosts = None self._enhance_func = {"redirect": self._enable_redirect} + @property + def mod_ssl_conf(self): + return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) + def prepare(self): """Prepare the authenticator/installer.""" self.parser = parser.ApacheParser( - self.aug, self.conf('server-root'), self.conf('mod-ssl-conf')) + self.aug, self.conf('server-root'), self.mod_ssl_conf) # Check for errors in parsing files with Augeas self.check_parsing_errors("httpd.aug") @@ -147,7 +149,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # on initialization self._prepare_server_https() - temp_install(self.conf('mod-ssl-conf')) + temp_install(self.mod_ssl_conf) def deploy_cert(self, domain, cert_path, key_path, chain_path=None): """Deploys certificate to specified virtual host. @@ -1171,4 +1173,4 @@ def temp_install(options_ssl): # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): - shutil.copyfile(constants.MOD_SSL_CONF, options_ssl) + shutil.copyfile(constants.MOD_SSL_CONF_SRC, options_ssl) diff --git a/letsencrypt_apache/constants.py b/letsencrypt_apache/constants.py index 865741823..c78df7808 100644 --- a/letsencrypt_apache/constants.py +++ b/letsencrypt_apache/constants.py @@ -4,7 +4,6 @@ import pkg_resources CLI_DEFAULTS = dict( server_root="/etc/apache2", - mod_ssl_conf="/etc/letsencrypt/options-ssl.conf", ctl="apache2ctl", enmod="a2enmod", init_script="/etc/init.d/apache2", @@ -12,8 +11,10 @@ CLI_DEFAULTS = dict( ) """CLI defaults.""" +MOD_SSL_CONF_DEST = "options-ssl-apache.conf" +"""Name of the mod_ssl config file as saved in `IConfig.config_dir`.""" -MOD_SSL_CONF = pkg_resources.resource_filename( +MOD_SSL_CONF_SRC = pkg_resources.resource_filename( "letsencrypt_apache", "options-ssl.conf") """Path to the Apache mod_ssl config file found in the Let's Encrypt distribution.""" diff --git a/letsencrypt_apache/tests/util.py b/letsencrypt_apache/tests/util.py index a5e700682..0b0367505 100644 --- a/letsencrypt_apache/tests/util.py +++ b/letsencrypt_apache/tests/util.py @@ -74,7 +74,6 @@ def get_apache_configurator( config = configurator.ApacheConfigurator( config=mock.MagicMock( apache_server_root=config_path, - apache_mod_ssl_conf=ssl_options, apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"], config_dir=config_dir, work_dir=work_dir), diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index f7b53f3fa..521a4facf 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -56,8 +56,6 @@ class NginxConfigurator(common.Plugin): def add_parser_arguments(cls, add): add("server-root", default=constants.CLI_DEFAULTS["server_root"], help="Nginx server root directory.") - add("mod-ssl-conf", default=constants.CLI_DEFAULTS["mod_ssl_conf"], - help="Contains standard nginx SSL directives.") add("ctl", default=constants.CLI_DEFAULTS["ctl"], help="Path to the " "'nginx' binary, used for 'configtest' and retrieving nginx " "version number.") @@ -91,18 +89,21 @@ class NginxConfigurator(common.Plugin): self.reverter = reverter.Reverter(self.config) self.reverter.recovery_routine() + @property + def mod_ssl_conf(self): + return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) + # This is called in determine_authenticator and determine_installer def prepare(self): """Prepare the authenticator/installer.""" self.parser = parser.NginxParser( - self.conf('server-root'), - self.conf('mod-ssl-conf')) + self.conf('server-root'), self.mod_ssl_conf) # Set Version if self.version is None: self.version = self.get_version() - temp_install(self.conf('mod-ssl-conf')) + temp_install(self.mod_ssl_conf) # Entry point in main.py for installing cert def deploy_cert(self, domain, cert_path, key_path, chain_path=None): @@ -592,4 +593,4 @@ def temp_install(options_ssl): # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): - shutil.copyfile(constants.MOD_SSL_CONF, options_ssl) + shutil.copyfile(constants.MOD_SSL_CONF_SRC, options_ssl) diff --git a/letsencrypt_nginx/constants.py b/letsencrypt_nginx/constants.py index 6c15b1664..055a35403 100644 --- a/letsencrypt_nginx/constants.py +++ b/letsencrypt_nginx/constants.py @@ -4,13 +4,15 @@ import pkg_resources CLI_DEFAULTS = dict( server_root="/etc/nginx", - mod_ssl_conf="/etc/letsencrypt/options-ssl-nginx.conf", ctl="nginx", ) """CLI defaults.""" -MOD_SSL_CONF = pkg_resources.resource_filename( +MOD_SSL_CONF_DEST = "options-ssl-nginx.conf" +"""Name of the mod_ssl config file as saved in `IConfig.config_dir`.""" + +MOD_SSL_CONF_SRC = pkg_resources.resource_filename( "letsencrypt_nginx", "options-ssl.conf") -"""Path to the Nginx mod_ssl config file found in the Let's Encrypt +"""Path to the nginx mod_ssl config file found in the Let's Encrypt distribution.""" diff --git a/letsencrypt_nginx/tests/util.py b/letsencrypt_nginx/tests/util.py index fd1418aa3..caa9de14e 100644 --- a/letsencrypt_nginx/tests/util.py +++ b/letsencrypt_nginx/tests/util.py @@ -44,9 +44,8 @@ def get_nginx_configurator( backups = os.path.join(work_dir, "backups") config = configurator.NginxConfigurator( - config=mock.MagicMock( - nginx_server_root=config_path, nginx_mod_ssl_conf=ssl_options, - config_dir=config_dir, work_dir=work_dir), + config=mock.MagicMock(nginx_server_root=config_path, + config_dir=config_dir, work_dir=work_dir), name="nginx", version=version) config.prepare() From c440b0354da8afe4a636e5c2f51137d2e7fcc6dc Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 00:16:24 +0000 Subject: [PATCH 007/109] Fix typo --- letsencrypt/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 6e2355252..a360e6f3d 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -73,7 +73,7 @@ IN_PROGRESS_DIR = "IN_PROGRESS" """Directory used before a permanent checkpoint is finalized (relative to `IConfig.work_dir`).""" -KEYS_DIR = "keys" +KEY_DIR = "keys" """Directory (relative to `IConfig.config_dir`) where keys are saved.""" TEMP_CHECKPOINT_DIR = "temp_checkpoint" From 8fed612feff5ceb42373f5224d5c6076ce02a537 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 00:40:33 +0000 Subject: [PATCH 008/109] Fix apache/nginx tests. --- letsencrypt_apache/tests/util.py | 10 +++++++--- letsencrypt_nginx/tests/util.py | 14 +++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/letsencrypt_apache/tests/util.py b/letsencrypt_apache/tests/util.py index 0b0367505..be4ca823e 100644 --- a/letsencrypt_apache/tests/util.py +++ b/letsencrypt_apache/tests/util.py @@ -54,10 +54,11 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80", def setup_ssl_options( - config_dir, mod_ssl_conf=constants.MOD_SSL_CONF): + config_dir, src=constants.MOD_SSL_CONF_SRC, + dest=constants.MOD_SSL_CONF_DEST): """Move the ssl_options into position and return the path.""" - option_path = os.path.join(config_dir, "options-ssl.conf") - shutil.copyfile(mod_ssl_conf, option_path) + option_path = os.path.join(config_dir, dest) + shutil.copyfile(src, option_path) return option_path @@ -75,7 +76,10 @@ def get_apache_configurator( 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) diff --git a/letsencrypt_nginx/tests/util.py b/letsencrypt_nginx/tests/util.py index caa9de14e..ea1ef0a6c 100644 --- a/letsencrypt_nginx/tests/util.py +++ b/letsencrypt_nginx/tests/util.py @@ -20,7 +20,8 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods "etc_nginx", "letsencrypt_nginx.tests") self.ssl_options = apache_util.setup_ssl_options( - self.config_dir, constants.MOD_SSL_CONF) + self.config_dir, constants.MOD_SSL_CONF_SRC, + constants.MOD_SSL_CONF_DEST) self.config_path = os.path.join(self.temp_dir, "etc_nginx") @@ -44,8 +45,15 @@ def get_nginx_configurator( backups = os.path.join(work_dir, "backups") config = configurator.NginxConfigurator( - config=mock.MagicMock(nginx_server_root=config_path, - config_dir=config_dir, work_dir=work_dir), + config=mock.MagicMock( + nginx_server_root=config_path, + le_vhost_ext="-le-ssl.conf", + config_dir=config_dir, + work_dir=work_dir, + backup_dir=backups, + temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), + in_progress_dir=os.path.join(backups, "IN_PROGRESS"), + ), name="nginx", version=version) config.prepare() From cd7a5ec24a0dcde681d683c6ecd6eea0424c5389 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 01:01:29 +0000 Subject: [PATCH 009/109] Fix pylint --- letsencrypt_apache/tests/configurator_test.py | 3 +-- letsencrypt_apache/tests/dvsni_test.py | 3 +-- letsencrypt_apache/tests/util.py | 2 +- letsencrypt_nginx/tests/configurator_test.py | 3 +-- letsencrypt_nginx/tests/dvsni_test.py | 3 +-- letsencrypt_nginx/tests/util.py | 2 +- 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/tests/configurator_test.py index 11b88f9e5..e732e1bce 100644 --- a/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt_apache/tests/configurator_test.py @@ -31,8 +31,7 @@ class TwoVhost80Test(util.ApacheTest): "mod_loaded") as mock_load: mock_load.return_value = True self.config = util.get_apache_configurator( - self.config_path, self.config_dir, self.work_dir, - self.ssl_options) + self.config_path, self.config_dir, self.work_dir) self.vh_truth = util.get_vh_truth( self.temp_dir, "debian_apache_2_4/two_vhost_80") diff --git a/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt_apache/tests/dvsni_test.py index 321dce42c..088ac9557 100644 --- a/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt_apache/tests/dvsni_test.py @@ -26,8 +26,7 @@ class DvsniPerformTest(util.ApacheTest): "mod_loaded") as mock_load: mock_load.return_value = True config = util.get_apache_configurator( - self.config_path, self.config_dir, self.work_dir, - self.ssl_options) + self.config_path, self.config_dir, self.work_dir) from letsencrypt_apache import dvsni self.sni = dvsni.ApacheDvsni(config) diff --git a/letsencrypt_apache/tests/util.py b/letsencrypt_apache/tests/util.py index be4ca823e..b477f0584 100644 --- a/letsencrypt_apache/tests/util.py +++ b/letsencrypt_apache/tests/util.py @@ -63,7 +63,7 @@ def setup_ssl_options( def get_apache_configurator( - config_path, config_dir, work_dir, ssl_options, version=(2, 4, 7)): + config_path, config_dir, work_dir, version=(2, 4, 7)): """Create an Apache Configurator with the specified options.""" backups = os.path.join(work_dir, "backups") diff --git a/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt_nginx/tests/configurator_test.py index 82b80b9d2..a5a1c416b 100644 --- a/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt_nginx/tests/configurator_test.py @@ -21,8 +21,7 @@ class NginxConfiguratorTest(util.NginxTest): super(NginxConfiguratorTest, self).setUp() self.config = util.get_nginx_configurator( - self.config_path, self.config_dir, self.work_dir, - self.ssl_options) + self.config_path, self.config_dir, self.work_dir) def tearDown(self): shutil.rmtree(self.temp_dir) diff --git a/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt_nginx/tests/dvsni_test.py index 1ea7793cc..1a49da944 100644 --- a/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt_nginx/tests/dvsni_test.py @@ -23,8 +23,7 @@ class DvsniPerformTest(util.NginxTest): super(DvsniPerformTest, self).setUp() config = util.get_nginx_configurator( - self.config_path, self.config_dir, self.work_dir, - self.ssl_options) + self.config_path, self.config_dir, self.work_dir) rsa256_file = pkg_resources.resource_filename( "acme.jose", "testdata/rsa256_key.pem") diff --git a/letsencrypt_nginx/tests/util.py b/letsencrypt_nginx/tests/util.py index ea1ef0a6c..f0aa56c28 100644 --- a/letsencrypt_nginx/tests/util.py +++ b/letsencrypt_nginx/tests/util.py @@ -39,7 +39,7 @@ def get_data_filename(filename): def get_nginx_configurator( - config_path, config_dir, work_dir, ssl_options, version=(1, 6, 2)): + config_path, config_dir, work_dir, version=(1, 6, 2)): """Create an Nginx Configurator with the specified options.""" backups = os.path.join(work_dir, "backups") From 9ecfecbc7ab9434ae719751e23837ebae5bea658 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 09:19:51 +0000 Subject: [PATCH 010/109] Add missing docstring. --- letsencrypt_apache/configurator.py | 1 + letsencrypt_nginx/configurator.py | 1 + 2 files changed, 2 insertions(+) diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 965e9cf73..078e61564 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -126,6 +126,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): @property def mod_ssl_conf(self): + """Full absolute path to SSL configuration file.""" return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) def prepare(self): diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index 521a4facf..87beb3c8f 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -91,6 +91,7 @@ class NginxConfigurator(common.Plugin): @property def mod_ssl_conf(self): + """Full absolute path to SSL configuration file.""" return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) # This is called in determine_authenticator and determine_installer From 0b57daf473f5f742eacbad7c999631987b4f9064 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 12:10:22 +0000 Subject: [PATCH 011/109] Renewer dynamic dirs based on --config-dir/--work-dir (fixes #469). --- letsencrypt/cli.py | 4 +- letsencrypt/client.py | 17 +++-- letsencrypt/configuration.py | 28 +++++++++ letsencrypt/constants.py | 11 ++-- letsencrypt/renewer.py | 20 ++++-- letsencrypt/storage.py | 37 +++++------ letsencrypt/tests/renewer_test.py | 101 +++++++++++++++--------------- 7 files changed, 137 insertions(+), 81 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 1a87f0c60..5978c4d21 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -283,7 +283,7 @@ def create_parser(plugins): help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") - _paths_parser(parser.add_argument_group("paths")) + _paths_parser(parser) # _plugins_parsing should be the last thing to act upon the main # parser (--help should display plugin-specific options last) _plugins_parsing(parser, plugins) @@ -342,7 +342,7 @@ def _create_subparsers(parser): def _paths_parser(parser): - add = parser.add_argument + add = parser.add_argument_group("paths").add_argument add("--config-dir", default=flag_default("config_dir"), help=config_help("config_dir")) add("--work-dir", default=flag_default("work_dir"), diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 02159f5d2..54077d644 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -11,6 +11,8 @@ from acme.jose import jwk from letsencrypt import account from letsencrypt import auth_handler +from letsencrypt import configuration +from letsencrypt import constants from letsencrypt import continuity_auth from letsencrypt import crypto_util from letsencrypt import errors @@ -193,10 +195,17 @@ class Client(object): # ideally should be a ConfigObj, but in this case a dict will be # accepted in practice.) params = vars(self.config.namespace) - config = {"renewer_config_file": - params["renewer_config_file"]} if "renewer_config_file" in params else None - return storage.RenewableCert.new_lineage(domains[0], cert, privkey, - chain, params, config) + config = {} + cli_config = configuration.RenewerConfiguration(self.config.namespace) + + if (cli_config.config_dir != constants.CLI_DEFAULTS["config_dir"] or + cli_config.work_dir != constants.CLI_DEFAULTS["work_dir"]): + logging.warning( + "Non-standard path(s), might not work with crontab installed " + "by your operating system package manager") + + return storage.RenewableCert.new_lineage( + domains[0], cert, privkey, chain, params, config, cli_config) def save_certificate(self, certr, cert_path, chain_path): diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 00b45040a..6f05b2b49 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -90,3 +90,31 @@ class NamespaceConfig(object): def temp_checkpoint_dir(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) + + +class RenewerConfiguration(object): + """Configuration wrapper for renewer.""" + + def __init__(self, namespace): + self.namespace = namespace + + def __getattr__(self, name): + return getattr(self.namespace, name) + + @property + def archive_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR) + + @property + def live_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.LIVE_DIR) + + @property + def renewal_configs_dir(self): # pylint: disable=missing-docstring + return os.path.join( + self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR) + + @property + def renewer_config_file(self): # pylint: disable=missing-docstring + return os.path.join( + self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 9d04fb4c2..56d91f0c9 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -22,10 +22,6 @@ CLI_DEFAULTS = dict( RENEWER_DEFAULTS = dict( - renewer_config_file="/etc/letsencrypt/renewer.conf", - renewal_configs_dir="/etc/letsencrypt/configs", - archive_dir="/etc/letsencrypt/archive", - live_dir="/etc/letsencrypt/live", renewer_enabled="yes", renew_before_expiry="30 days", deploy_before_expiry="20 days", @@ -50,6 +46,8 @@ List of expected options parameters: """ +ARCHIVE_DIR = "archive" +"""TODO relative to `IConfig.config_dir`.""" CONFIG_DIRS_MODE = 0o755 """Directory mode for ``.IConfig.config_dir`` et al.""" @@ -77,6 +75,9 @@ IN_PROGRESS_DIR = "IN_PROGRESS" KEY_DIR = "keys" """Directory (relative to `IConfig.config_dir`) where keys are saved.""" +LIVE_DIR = "live" +"""TODO relative to `IConfig.config_dir`.""" + TEMP_CHECKPOINT_DIR = "temp_checkpoint" """Temporary checkpoint directory (relative to `IConfig.work_dir`).""" @@ -84,6 +85,8 @@ REC_TOKEN_DIR = "recovery_tokens" """Directory where all recovery tokens are saved (relative to `IConfig.work_dir`).""" +RENEWAL_CONFIGS_DIR = "configs" +"""TODO relative to `IConfig.config_dir`.""" RENEWER_CONFIG_FILENAME = "renewer.conf" """Renewer config file name (relative to `IConfig.config_dir`).""" diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 6e61fd893..b27f5fa4c 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -7,15 +7,19 @@ within lineages of successor certificates, according to configuration. .. todo:: Call new installer API to restart servers after deployment """ +import argparse import os +import sys import configobj from letsencrypt import configuration +from letsencrypt import cli from letsencrypt import client from letsencrypt import crypto_util from letsencrypt import notify from letsencrypt import storage + from letsencrypt.plugins import disco as plugins_disco @@ -92,7 +96,12 @@ def renew(cert, old_version): # (where fewer than all names were renewed) -def main(config=None): +def _create_parser(): + parser = argparse.ArgumentParser() + #parser.add_argument("--cron", action="store_true", help="Run as cronjob.") + return cli._paths_parser(parser) # pylint: disable=protected-access + +def main(config=None, args=sys.argv[1:]): """Main function for autorenewer script.""" # TODO: Distinguish automated invocation from manual invocation, # perhaps by looking at sys.argv[0] and inhibiting automated @@ -100,6 +109,9 @@ def main(config=None): # turned it off. (The boolean parameter should probably be # called renewer_enabled.) + cli_config = configuration.RenewerConfiguration( + _create_parser().parse_args(args)) + config = storage.config_with_defaults(config) # Now attempt to read the renewer config file and augment or replace # the renewer defaults with any options contained in that file. If @@ -108,14 +120,14 @@ def main(config=None): # elaborate renewer command line, we will presumably also be able to # specify a config file on the command line, which, if provided, should # take precedence over this one. - config.merge(configobj.ConfigObj(config.get("renewer_config_file", ""))) + config.merge(configobj.ConfigObj(cli_config.renewer_config_file)) - for i in os.listdir(config["renewal_configs_dir"]): + for i in os.listdir(cli_config.renewal_configs_dir): print "Processing", i if not i.endswith(".conf"): continue rc_config = configobj.ConfigObj( - os.path.join(config["renewal_configs_dir"], i)) + os.path.join(cli_config.renewal_configs_dir, i)) try: # TODO: Before trying to initialize the RenewableCert object, # we could check here whether the combination of the config diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 2648be3ba..c314e3b00 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -78,14 +78,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes renewal configuration file and/or systemwide defaults. """ - def __init__(self, configfile, config_opts=None): + def __init__(self, configfile, config_opts=None, cli_config=None): """Instantiate a RenewableCert object from an existing lineage. :param configobj.ConfigObj configfile: an already-parsed - ConfigObj object made from reading the renewal config file - that defines this lineage. :param configobj.ConfigObj - config_opts: systemwide defaults for renewal properties not - otherwise specified in the individual renewal config file. + ConfigObj object made from reading the renewal config file + that defines this lineage. + + :param configobj.ConfigObj config_opts: systemwide defaults for + renewal properties not otherwise specified in the individual + renewal config file. :raises ValueError: if the configuration file's name didn't end in ".conf", or the file is missing or broken. @@ -93,6 +95,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes ConfigObj object. """ + self.cli_config = cli_config if isinstance(configfile, configobj.ConfigObj): if not os.path.basename(configfile.filename).endswith(".conf"): raise ValueError("renewal config file name must end in .conf") @@ -149,7 +152,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Each element's link must point within the cert lineage's # directory within the official archive directory desired_directory = os.path.join( - self.configuration["archive_dir"], self.lineagename) + self.cli_config.archive_dir, self.lineagename) if not os.path.samefile(os.path.dirname(target), desired_directory): return False @@ -499,7 +502,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes @classmethod def new_lineage(cls, lineagename, cert, privkey, chain, - renewalparams=None, config=None): + renewalparams=None, config=None, cli_config=None): # pylint: disable=too-many-locals,too-many-arguments """Create a new certificate lineage. @@ -536,17 +539,15 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # the renewer defaults with any options contained in that file. If # renewer_config_file is undefined or if the file is nonexistent or # empty, this .merge() will have no effect. - config.merge(configobj.ConfigObj(config.get("renewer_config_file", ""))) + config.merge(configobj.ConfigObj(cli_config.renewer_config_file)) # Examine the configuration and find the new lineage's name - configs_dir = config["renewal_configs_dir"] - archive_dir = config["archive_dir"] - live_dir = config["live_dir"] - for i in (configs_dir, archive_dir, live_dir): + for i in (cli_config.renewal_configs_dir, cli_config.archive_dir, + cli_config.live_dir): if not os.path.exists(i): os.makedirs(i, 0700) - config_file, config_filename = le_util.unique_lineage_name(configs_dir, - lineagename) + config_file, config_filename = le_util.unique_lineage_name( + cli_config.renewal_configs_dir, lineagename) if not config_filename.endswith(".conf"): raise ValueError("renewal config file name must end in .conf") @@ -554,8 +555,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # lineagename will now potentially be modified based on which # renewal configuration file could actually be created lineagename = os.path.basename(config_filename)[:-len(".conf")] - archive = os.path.join(archive_dir, lineagename) - live_dir = os.path.join(live_dir, lineagename) + archive = os.path.join(cli_config.archive_dir, lineagename) + live_dir = os.path.join(cli_config.live_dir, lineagename) if os.path.exists(archive): raise ValueError("archive directory exists for " + lineagename) if os.path.exists(live_dir): @@ -593,7 +594,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # TODO: add human-readable comments explaining other available # parameters new_config.write() - return cls(new_config, config) + return cls(new_config, config, cli_config) def save_successor(self, prior_version, new_cert, new_privkey, new_chain): @@ -624,7 +625,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Figure out what the new version is and hence where to save things target_version = self.next_free_version() - archive = self.configuration["archive_dir"] + archive = self.cli_config.archive_dir prefix = os.path.join(archive, self.lineagename) target = dict( [(kind, diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 0f85674d4..d68078c18 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -10,6 +10,7 @@ import configobj import mock import pytz +from letsencrypt import configuration from letsencrypt.storage import ALL_FOUR @@ -31,22 +32,24 @@ class RenewableCertTests(unittest.TestCase): def setUp(self): from letsencrypt import storage self.tempdir = tempfile.mkdtemp() + + self.cli_config = configuration.RenewerConfiguration( + namespace=mock.MagicMock(config_dir=self.tempdir)) + # TODO: maybe provide RenewerConfiguration.make_dirs? os.makedirs(os.path.join(self.tempdir, "live", "example.org")) os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) os.makedirs(os.path.join(self.tempdir, "configs")) - defaults = configobj.ConfigObj() - defaults["live_dir"] = os.path.join(self.tempdir, "live") - defaults["archive_dir"] = os.path.join(self.tempdir, "archive") - defaults["renewal_configs_dir"] = os.path.join(self.tempdir, - "configs") + config = configobj.ConfigObj() for kind in ALL_FOUR: config[kind] = os.path.join(self.tempdir, "live", "example.org", kind + ".pem") config.filename = os.path.join(self.tempdir, "configs", "example.org.conf") - self.defaults = defaults # for main() test - self.test_rc = storage.RenewableCert(config, defaults) + + self.defaults = configobj.ConfigObj() + self.test_rc = storage.RenewableCert( + config, self.defaults, self.cli_config) def tearDown(self): shutil.rmtree(self.tempdir) @@ -457,60 +460,57 @@ class RenewableCertTests(unittest.TestCase): def test_new_lineage(self): """Test for new_lineage() class method.""" from letsencrypt import storage - config_dir = self.defaults["renewal_configs_dir"] - archive_dir = self.defaults["archive_dir"] - live_dir = self.defaults["live_dir"] - result = storage.RenewableCert.new_lineage("the-lineage.com", "cert", - "privkey", "chain", None, - self.defaults) + result = storage.RenewableCert.new_lineage( + "the-lineage.com", "cert", "privkey", "chain", None, + self.defaults, self.cli_config) # This consistency check tests most relevant properties about the # newly created cert lineage. self.assertTrue(result.consistent()) - self.assertTrue(os.path.exists(os.path.join(config_dir, - "the-lineage.com.conf"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) with open(result.fullchain) as f: self.assertEqual(f.read(), "cert" + "chain") # Let's do it again and make sure it makes a different lineage - result = storage.RenewableCert.new_lineage("the-lineage.com", "cert2", - "privkey2", "chain2", None, - self.defaults) - self.assertTrue(os.path.exists( - os.path.join(config_dir, "the-lineage.com-0001.conf"))) + result = storage.RenewableCert.new_lineage( + "the-lineage.com", "cert2", "privkey2", "chain2", None, + self.defaults, self.cli_config) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) # Now trigger the detection of already existing files - os.mkdir(os.path.join(live_dir, "the-lineage.com-0002")) + os.mkdir(os.path.join( + self.cli_config.live_dir, "the-lineage.com-0002")) self.assertRaises(ValueError, storage.RenewableCert.new_lineage, "the-lineage.com", "cert3", "privkey3", "chain3", - None, self.defaults) - os.mkdir(os.path.join(archive_dir, "other-example.com")) + None, self.defaults, self.cli_config) + os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com")) self.assertRaises(ValueError, storage.RenewableCert.new_lineage, "other-example.com", "cert4", "privkey4", "chain4", - None, self.defaults) + None, self.defaults, self.cli_config) # Make sure it can accept renewal parameters params = {"stuff": "properties of stuff", "great": "awesome"} - result = storage.RenewableCert.new_lineage("the-lineage.com", "cert2", - "privkey2", "chain2", - params, self.defaults) + result = storage.RenewableCert.new_lineage( + "the-lineage.com", "cert2", "privkey2", "chain2", + params, self.defaults, self.cli_config) # TODO: Conceivably we could test that the renewal parameters actually # got saved def test_new_lineage_nonexistent_dirs(self): """Test that directories can be created if they don't exist.""" from letsencrypt import storage - config_dir = self.defaults["renewal_configs_dir"] - archive_dir = self.defaults["archive_dir"] - live_dir = self.defaults["live_dir"] - shutil.rmtree(config_dir) - shutil.rmtree(archive_dir) - shutil.rmtree(live_dir) - storage.RenewableCert.new_lineage("the-lineage.com", "cert2", - "privkey2", "chain2", - None, self.defaults) + shutil.rmtree(self.cli_config.renewal_configs_dir) + shutil.rmtree(self.cli_config.archive_dir) + shutil.rmtree(self.cli_config.live_dir) + + storage.RenewableCert.new_lineage( + "the-lineage.com", "cert2", "privkey2", "chain2", + None, self.defaults, self.cli_config) self.assertTrue(os.path.exists( - os.path.join(config_dir, "the-lineage.com.conf"))) - self.assertTrue(os.path.exists( - os.path.join(live_dir, "the-lineage.com", "privkey.pem"))) - self.assertTrue(os.path.exists( - os.path.join(archive_dir, "the-lineage.com", "privkey1.pem"))) + os.path.join( + self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.live_dir, "the-lineage.com", "privkey.pem"))) + self.assertTrue(os.path.exists(os.path.join( + self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem"))) @mock.patch("letsencrypt.storage.le_util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): @@ -518,7 +518,7 @@ class RenewableCertTests(unittest.TestCase): mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" self.assertRaises(ValueError, storage.RenewableCert.new_lineage, "example.com", "cert", "privkey", "chain", - None, self.defaults) + None, self.defaults, self.cli_config) def test_bad_kind(self): self.assertRaises(ValueError, self.test_rc.current_target, "elephant") @@ -602,22 +602,23 @@ class RenewableCertTests(unittest.TestCase): mock_rc_instance.should_autorenew.return_value = True mock_rc_instance.latest_common_version.return_value = 10 mock_rc.return_value = mock_rc_instance - with open(os.path.join(self.defaults["renewal_configs_dir"], + with open(os.path.join(self.cli_config.renewal_configs_dir, "README"), "w") as f: f.write("This is a README file to make sure that the renewer is") f.write("able to correctly ignore files that don't end in .conf.") - with open(os.path.join(self.defaults["renewal_configs_dir"], + with open(os.path.join(self.cli_config.renewal_configs_dir, "example.org.conf"), "w") as f: # This isn't actually parsed in this test; we have a separate # test_initialization that tests the initialization, assuming # that configobj can correctly parse the config file. f.write("cert = cert.pem\nprivkey = privkey.pem\n") f.write("chain = chain.pem\nfullchain = fullchain.pem\n") - with open(os.path.join(self.defaults["renewal_configs_dir"], + with open(os.path.join(self.cli_config.renewal_configs_dir, "example.com.conf"), "w") as f: f.write("cert = cert.pem\nprivkey = privkey.pem\n") f.write("chain = chain.pem\nfullchain = fullchain.pem\n") - renewer.main(self.defaults) + renewer.main(self.defaults, args=[ + '--config-dir', self.cli_config.config_dir]) self.assertEqual(mock_rc.call_count, 2) self.assertEqual(mock_rc_instance.update_all_links_to.call_count, 2) self.assertEqual(mock_notify.notify.call_count, 4) @@ -630,7 +631,8 @@ class RenewableCertTests(unittest.TestCase): mock_happy_instance.should_autorenew.return_value = False mock_happy_instance.latest_common_version.return_value = 10 mock_rc.return_value = mock_happy_instance - renewer.main(self.defaults) + renewer.main(self.defaults, args=[ + '--config-dir', self.cli_config.config_dir]) self.assertEqual(mock_rc.call_count, 4) self.assertEqual(mock_happy_instance.update_all_links_to.call_count, 0) self.assertEqual(mock_notify.notify.call_count, 4) @@ -638,10 +640,11 @@ class RenewableCertTests(unittest.TestCase): def test_bad_config_file(self): from letsencrypt import renewer - with open(os.path.join(self.defaults["renewal_configs_dir"], + with open(os.path.join(self.cli_config.renewal_configs_dir, "bad.conf"), "w") as f: f.write("incomplete = configfile\n") - renewer.main(self.defaults) + renewer.main(self.defaults, args=[ + '--config-dir', self.cli_config.config_dir]) # The ValueError is caught inside and nothing happens. From 8c6d1ad50aa95091790be8f9d6e289f6c662b7c4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 13:55:16 +0000 Subject: [PATCH 012/109] letsencrypt_nginx should not depend on letsencrypt_apache. --- letsencrypt/plugins/common.py | 130 ++++++++++++++++++ letsencrypt/plugins/common_test.py | 106 ++++++++++++++ letsencrypt_apache/configurator.py | 12 +- letsencrypt_apache/dvsni.py | 57 +------- letsencrypt_apache/obj.py | 45 +----- letsencrypt_apache/tests/configurator_test.py | 7 +- letsencrypt_apache/tests/dvsni_test.py | 63 +-------- letsencrypt_apache/tests/obj_test.py | 50 +------ letsencrypt_apache/tests/util.py | 48 ++----- letsencrypt_nginx/dvsni.py | 5 +- letsencrypt_nginx/obj.py | 4 +- letsencrypt_nginx/tests/dvsni_test.py | 66 ++++----- letsencrypt_nginx/tests/util.py | 6 +- 13 files changed, 313 insertions(+), 286 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 32bee2b49..90296c5c7 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -1,8 +1,14 @@ """Plugin common functions.""" +import os +import pkg_resources +import shutil +import tempfile + import zope.interface from acme.jose import util as jose_util +from letsencrypt import constants from letsencrypt import interfaces @@ -69,3 +75,127 @@ class Plugin(object): with unique plugin name prefix. """ + +# other + +class Addr(object): + r"""Represents an virtual host address. + + :param str addr: addr part of vhost address + :param str port: port number or \*, or "" + + """ + def __init__(self, tup): + self.tup = tup + + @classmethod + def fromstring(cls, str_addr): + """Initialize Addr from string.""" + tup = str_addr.partition(':') + return cls((tup[0], tup[2])) + + def __str__(self): + if self.tup[1]: + return "%s:%s" % self.tup + return self.tup[0] + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.tup == other.tup + return False + + def __hash__(self): + return hash(self.tup) + + def get_addr(self): + """Return addr part of Addr object.""" + return self.tup[0] + + def get_port(self): + """Return port.""" + return self.tup[1] + + def get_addr_obj(self, port): + """Return new address object with same addr and new port.""" + return self.__class__((self.tup[0], port)) + + +class Dvsni(object): + """Class that perform DVSNI challenges.""" + + def __init__(self, configurator): + self.configurator = configurator + self.achalls = [] + self.indices = [] + self.challenge_conf = os.path.join( + configurator.config.config_dir, "le_dvsni_cert_challenge.conf") + # self.completed = 0 + + def add_chall(self, achall, idx=None): + """Add challenge to DVSNI object to perform at once. + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.achallenges.DVSNI` + + :param int idx: index to challenge in a larger array + + """ + self.achalls.append(achall) + if idx is not None: + self.indices.append(idx) + + def get_cert_file(self, achall): + """Returns standardized name for challenge certificate. + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.achallenges.DVSNI` + + :returns: certificate file name + :rtype: str + + """ + return os.path.join( + self.configurator.config.work_dir, achall.nonce_domain + ".crt") + + def _setup_challenge_cert(self, achall, s=None): + # pylint: disable=invalid-name + """Generate and write out challenge certificate.""" + cert_path = self.get_cert_file(achall) + # Register the path before you write out the file + self.configurator.reverter.register_file_creation(True, cert_path) + + cert_pem, response = achall.gen_cert_and_response(s) + + # Write out challenge cert + with open(cert_path, "w") as cert_chall_fd: + cert_chall_fd.write(cert_pem) + + return response + + +# test utils + +def setup_ssl_options(config_dir, mod_ssl_conf): + """Move the ssl_options into position and return the path.""" + option_path = os.path.join(config_dir, "options-ssl.conf") + shutil.copyfile(mod_ssl_conf, option_path) + return option_path + + +def dir_setup(test_dir, pkg): + """Setup the directories necessary for the configurator.""" + temp_dir = tempfile.mkdtemp("temp") + config_dir = tempfile.mkdtemp("config") + work_dir = tempfile.mkdtemp("work") + + os.chmod(temp_dir, constants.CONFIG_DIRS_MODE) + os.chmod(config_dir, constants.CONFIG_DIRS_MODE) + os.chmod(work_dir, constants.CONFIG_DIRS_MODE) + + test_configs = pkg_resources.resource_filename( + pkg, os.path.join("testdata", test_dir)) + + shutil.copytree( + test_configs, os.path.join(temp_dir, test_dir), symlinks=True) + + return temp_dir, config_dir, work_dir diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 12dd18bdf..6de86f2b8 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -1,8 +1,16 @@ """Tests for letsencrypt.plugins.common.""" +import pkg_resources import unittest import mock +from acme import challenges + +from letsencrypt import achallenges +from letsencrypt import le_util + +from letsencrypt.tests import acme_util + class NamespaceFunctionsTest(unittest.TestCase): """Tests for letsencrypt.plugins.common.*_namespace functions.""" @@ -57,5 +65,103 @@ class PluginTest(unittest.TestCase): "--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None) +class AddrTest(unittest.TestCase): + """Tests for letsencrypt.client.plugins.common.Addr.""" + + def setUp(self): + from letsencrypt.plugins.common import Addr + self.addr1 = Addr.fromstring("192.168.1.1") + self.addr2 = Addr.fromstring("192.168.1.1:*") + self.addr3 = Addr.fromstring("192.168.1.1:80") + + def test_fromstring(self): + self.assertEqual(self.addr1.get_addr(), "192.168.1.1") + self.assertEqual(self.addr1.get_port(), "") + self.assertEqual(self.addr2.get_addr(), "192.168.1.1") + self.assertEqual(self.addr2.get_port(), "*") + self.assertEqual(self.addr3.get_addr(), "192.168.1.1") + self.assertEqual(self.addr3.get_port(), "80") + + def test_str(self): + self.assertEqual(str(self.addr1), "192.168.1.1") + self.assertEqual(str(self.addr2), "192.168.1.1:*") + self.assertEqual(str(self.addr3), "192.168.1.1:80") + + def test_get_addr_obj(self): + self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443") + self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1") + self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*") + + def test_eq(self): + self.assertEqual(self.addr1, self.addr2.get_addr_obj("")) + self.assertNotEqual(self.addr1, self.addr2) + self.assertFalse(self.addr1 == 3333) + + def test_set_inclusion(self): + from letsencrypt.plugins.common import Addr + set_a = set([self.addr1, self.addr2]) + addr1b = Addr.fromstring("192.168.1.1") + addr2b = Addr.fromstring("192.168.1.1:*") + set_b = set([addr1b, addr2b]) + + self.assertEqual(set_a, set_b) + + +class DvsniTest(unittest.TestCase): + """Tests for letsencrypt.plugins.common.DvsniTest.""" + + rsa256_file = pkg_resources.resource_filename( + "acme.jose", "testdata/rsa256_key.pem") + rsa256_pem = pkg_resources.resource_string( + "acme.jose", "testdata/rsa256_key.pem") + + auth_key = le_util.Key(rsa256_file, rsa256_pem) + achalls = [ + achallenges.DVSNI( + challb=acme_util.chall_to_challb( + challenges.DVSNI( + r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9" + "\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", + nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", + ), "pending"), + domain="encryption-example.demo", key=auth_key), + achallenges.DVSNI( + challb=acme_util.chall_to_challb( + challenges.DVSNI( + r="\xba\xa9\xda? """ - def __init__(self, configurator): - self.configurator = configurator - self.achalls = [] - self.indices = [] - self.challenge_conf = os.path.join( - configurator.config.config_dir, "le_dvsni_cert_challenge.conf") - # self.completed = 0 - - def add_chall(self, achall, idx=None): - """Add challenge to DVSNI object to perform at once. - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` - - :param int idx: index to challenge in a larger array - - """ - self.achalls.append(achall) - if idx is not None: - self.indices.append(idx) def perform(self): """Peform a DVSNI challenge.""" @@ -107,28 +89,12 @@ class ApacheDvsni(object): return responses - def _setup_challenge_cert(self, achall, s=None): - # pylint: disable=invalid-name - """Generate and write out challenge certificate.""" - cert_path = self.get_cert_file(achall) - # Register the path before you write out the file - self.configurator.reverter.register_file_creation(True, cert_path) - - cert_pem, response = achall.gen_cert_and_response(s) - - # Write out challenge cert - with open(cert_path, "w") as cert_chall_fd: - cert_chall_fd.write(cert_pem) - - return response - def _mod_config(self, ll_addrs): """Modifies Apache config files to include challenge vhosts. Result: Apache config includes virtual servers for issued challs - :param list ll_addrs: list of list of - :class:`letsencrypt.plugins.apache.obj.Addr` to apply + :param list ll_addrs: list of list of `~.common.Addr` to apply """ # TODO: Use ip address of existing vhost instead of relying on FQDN @@ -167,7 +133,7 @@ class ApacheDvsni(object): :type achall: :class:`letsencrypt.achallenges.DVSNI` :param list ip_addrs: addresses of challenged domain - :class:`list` of type :class:`~apache.obj.Addr` + :class:`list` of type `~.common.Addr` :returns: virtual host configuration text :rtype: str @@ -186,16 +152,3 @@ class ApacheDvsni(object): ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], cert_path=self.get_cert_file(achall), key_path=achall.key.file, document_root=document_root).replace("\n", os.linesep) - - def get_cert_file(self, achall): - """Returns standardized name for challenge certificate. - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` - - :returns: certificate file name - :rtype: str - - """ - return os.path.join( - self.configurator.config.work_dir, achall.nonce_domain + ".crt") diff --git a/letsencrypt_apache/obj.py b/letsencrypt_apache/obj.py index 905e3f192..fecf46ff9 100644 --- a/letsencrypt_apache/obj.py +++ b/letsencrypt_apache/obj.py @@ -1,54 +1,13 @@ """Module contains classes used by the Apache Configurator.""" -class Addr(object): - r"""Represents an Apache VirtualHost address. - - :param str addr: addr part of vhost address - :param str port: port number or \*, or "" - - """ - def __init__(self, tup): - self.tup = tup - - @classmethod - def fromstring(cls, str_addr): - """Initialize Addr from string.""" - tup = str_addr.partition(':') - return cls((tup[0], tup[2])) - - def __str__(self): - if self.tup[1]: - return "%s:%s" % self.tup - return self.tup[0] - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.tup == other.tup - return False - - def __hash__(self): - return hash(self.tup) - - def get_addr(self): - """Return addr part of Addr object.""" - return self.tup[0] - - def get_port(self): - """Return port.""" - return self.tup[1] - - def get_addr_obj(self, port): - """Return new address object with same addr and new port.""" - return self.__class__((self.tup[0], port)) - - class VirtualHost(object): # pylint: disable=too-few-public-methods """Represents an Apache Virtualhost. :ivar str filep: file path of VH :ivar str path: Augeas path to virtual host - :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`) + :ivar set addrs: Virtual Host addresses (:class:`set` of + :class:`common.Addr`) :ivar set names: Server names/aliases of vhost (:class:`list` of :class:`str`) diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/tests/configurator_test.py index 11b88f9e5..c3064eb5b 100644 --- a/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt_apache/tests/configurator_test.py @@ -12,10 +12,11 @@ from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import le_util +from letsencrypt.plugins import common + from letsencrypt.tests import acme_util from letsencrypt_apache import configurator -from letsencrypt_apache import obj from letsencrypt_apache import parser from letsencrypt_apache.tests import util @@ -112,7 +113,7 @@ class TwoVhost80Test(util.ApacheTest): self.vh_truth[1].filep) def test_is_name_vhost(self): - addr = obj.Addr.fromstring("*:80") + addr = common.Addr.fromstring("*:80") self.assertTrue(self.config.is_name_vhost(addr)) self.config.version = (2, 2) self.assertFalse(self.config.is_name_vhost(addr)) @@ -133,7 +134,7 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(ssl_vhost.path, "/files" + ssl_vhost.filep + "/IfModule/VirtualHost") self.assertEqual(len(ssl_vhost.addrs), 1) - self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs) + self.assertEqual(set([common.Addr.fromstring("*:443")]), ssl_vhost.addrs) self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"])) self.assertTrue(ssl_vhost.ssl) self.assertFalse(ssl_vhost.enabled) diff --git a/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt_apache/tests/dvsni_test.py index 321dce42c..bf43bc359 100644 --- a/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt_apache/tests/dvsni_test.py @@ -1,5 +1,4 @@ """Test for letsencrypt_apache.dvsni.""" -import pkg_resources import unittest import shutil @@ -7,18 +6,17 @@ import mock from acme import challenges -from letsencrypt import achallenges -from letsencrypt import le_util +from letsencrypt.plugins import common +from letsencrypt.plugins import common_test -from letsencrypt.tests import acme_util - -from letsencrypt_apache import obj from letsencrypt_apache.tests import util class DvsniPerformTest(util.ApacheTest): """Test the ApacheDVSNI challenge.""" + achalls = common_test.DvsniTest.achalls + def setUp(self): super(DvsniPerformTest, self).setUp() @@ -32,32 +30,6 @@ class DvsniPerformTest(util.ApacheTest): from letsencrypt_apache import dvsni self.sni = dvsni.ApacheDvsni(config) - rsa256_file = pkg_resources.resource_filename( - "acme.jose", "testdata/rsa256_key.pem") - rsa256_pem = pkg_resources.resource_string( - "acme.jose", "testdata/rsa256_key.pem") - - auth_key = le_util.Key(rsa256_file, rsa256_pem) - self.achalls = [ - achallenges.DVSNI( - challb=acme_util.chall_to_challb( - challenges.DVSNI( - r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9" - "\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", - nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", - ), "pending"), - domain="encryption-example.demo", key=auth_key), - achallenges.DVSNI( - challb=acme_util.chall_to_challb( - challenges.DVSNI( - r="\xba\xa9\xda? Date: Tue, 2 Jun 2015 17:42:23 +0000 Subject: [PATCH 013/109] Rename cert_dir to csr_dir. --- letsencrypt/client.py | 2 +- letsencrypt/configuration.py | 10 +++++----- letsencrypt/constants.py | 2 +- letsencrypt/crypto_util.py | 8 ++++---- letsencrypt/interfaces.py | 2 +- letsencrypt/tests/configuration_test.py | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 02159f5d2..74ea529d2 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -134,7 +134,7 @@ class Client(object): cert_key = crypto_util.init_save_key( self.config.rsa_key_size, self.config.key_dir) csr = crypto_util.init_save_csr( - cert_key, domains, self.config.cert_dir) + cert_key, domains, self.config.csr_dir) # Retrieve certificate certr = self.network.request_issuance( diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 00b45040a..670db0e76 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -19,7 +19,7 @@ class NamespaceConfig(object): - `accounts_dir` - `account_keys_dir` - - `cert_dir` + - `csr_dir` - `cert_key_backup` - `in_progress_dir` - `key_dir` @@ -59,15 +59,15 @@ class NamespaceConfig(object): def backup_dir(self): # pylint: disable=missing-docstring 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) - @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 csr_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.CSR_DIR) + @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 9d04fb4c2..c407f8825 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -67,7 +67,7 @@ CERT_KEY_BACKUP_DIR = "keys-certs" """Directory where all certificates and keys are stored (relative to `IConfig.work_dir`). Used for easy revocation.""" -CERT_DIR = "certs" +CSR_DIR = "csrs" """Directory (relative to `IConfig.config_dir`) where CSRs are saved.""" IN_PROGRESS_DIR = "IN_PROGRESS" diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 1eb565289..9172fda46 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -55,7 +55,7 @@ def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"): return le_util.Key(key_path, key_pem) -def init_save_csr(privkey, names, cert_dir, csrname="csr-letsencrypt.pem"): +def init_save_csr(privkey, names, path, csrname="csr-letsencrypt.pem"): """Initialize a CSR with the given private key. :param privkey: Key to include in the CSR @@ -63,7 +63,7 @@ def init_save_csr(privkey, names, cert_dir, csrname="csr-letsencrypt.pem"): :param set names: `str` names to include in the CSR - :param str cert_dir: Certificate save directory. + :param str path: Certificate save directory. :returns: CSR :rtype: :class:`letsencrypt.le_util.CSR` @@ -72,9 +72,9 @@ def init_save_csr(privkey, names, cert_dir, csrname="csr-letsencrypt.pem"): csr_pem, csr_der = make_csr(privkey.pem, names) # Save CSR - le_util.make_or_verify_dir(cert_dir, 0o755, os.geteuid()) + le_util.make_or_verify_dir(path, 0o755, os.geteuid()) csr_f, csr_filename = le_util.unique_file( - os.path.join(cert_dir, csrname), 0o644) + os.path.join(path, csrname), 0o644) csr_f.write(csr_pem) csr_f.close() diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 7e9133ba3..17905149a 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -162,7 +162,7 @@ class IConfig(zope.interface.Interface): account_keys_dir = zope.interface.Attribute( "Directory where all account keys are stored.") backup_dir = zope.interface.Attribute("Configuration backups directory.") - cert_dir = zope.interface.Attribute("Certificates and CSRs storage.") + csr_dir = zope.interface.Attribute("CSRs storage.") cert_key_backup = zope.interface.Attribute( "Directory where all certificates and keys are stored. " "Used for easy revocation.") diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 345e3abbc..38fea140a 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -33,8 +33,8 @@ class NamespaceConfigTest(unittest.TestCase): constants.ACCOUNTS_DIR = 'acc' constants.ACCOUNT_KEYS_DIR = 'keys' constants.BACKUP_DIR = 'backups' - constants.CERT_DIR = 'certs' constants.CERT_KEY_BACKUP_DIR = 'c/' + constants.CSR_DIR = 'csrs' constants.IN_PROGRESS_DIR = '../p' constants.KEY_DIR = 'keys' constants.REC_TOKEN_DIR = '/r' @@ -47,7 +47,7 @@ class NamespaceConfigTest(unittest.TestCase): self.config.account_keys_dir, '/tmp/config/acc/acme-server.org:443/new/keys') 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/csrs') 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 f06c0017dbfcb82fd41486effde7825497d9499b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 17:49:13 +0000 Subject: [PATCH 014/109] Lower letsencrypt_apache coverage --- tox.cover.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.cover.sh b/tox.cover.sh index 80b6474d7..d1d030ed6 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -19,4 +19,4 @@ cover () { # 0, coveralls submit will be triggered (c.f. .travis.yml, # after_success) cover letsencrypt 95 && cover acme 100 && \ - cover letsencrypt_apache 78 && cover letsencrypt_nginx 96 + cover letsencrypt_apache 76 && cover letsencrypt_nginx 96 From 8cf9a152deac3ecc7dbf27e26724d2fb24f6feeb Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 4 Jun 2015 14:22:05 -0700 Subject: [PATCH 015/109] Don't suggest optional email. Email is still optional, by the same mechanism, but removing the suggestion to leave it out we will greatly increase the percentage of users that supply one, which in turn will reduce customer support requests. --- letsencrypt/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 3f8e3d012..93a949050 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -186,7 +186,7 @@ class Account(object): """ while True: code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address (optional, press Enter to skip)") + "Enter email address") if code == display_util.OK: try: From 3457a01da2942c5436bca4dbfdf7789562ad40fe Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 6 Jun 2015 15:45:17 -0700 Subject: [PATCH 016/109] WIP: Cleaning up the command line help --- letsencrypt/cli.py | 48 ++++++++++++++++++++--------------- letsencrypt/constants.py | 2 +- letsencrypt/plugins/common.py | 2 ++ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 4b0e271f7..2f3eabaec 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -135,7 +135,7 @@ def auth(args, config, plugins): def install(args, config, plugins): - """Install (no auth).""" + """Install cert in server software (no auth).""" # XXX: Update for renewer/RenewableCert acc = _account_init(args, config) if acc is None: @@ -224,9 +224,12 @@ def flag_default(name): """Default value for CLI flag.""" return constants.CLI_DEFAULTS[name] -def config_help(name): +def config_help(name, hidden=False): """Help message for `.IConfig` attribute.""" - return interfaces.IConfig[name].__doc__ + if hidden: + return argparse.SUPPRESS + else: + return interfaces.IConfig[name].__doc__ def create_parser(plugins): @@ -238,17 +241,19 @@ def create_parser(plugins): default_config_files=flag_default("config_files")) add = parser.add_argument + auto_group = parser.add_argument_group( + "auto", description="Arguments for automating execution") # --help is automatically provided by argparse - add("--version", action="version", version="%(prog)s {0}".format( - letsencrypt.__version__)) + auto_group.add_argument("--version", action="version", + version="%(prog)s {0}".format(letsencrypt.__version__)) add("-v", "--verbose", dest="verbose_count", action="count", default=flag_default("verbose_count"), help="This flag can be used " "multiple times to incrementally increase the verbosity of output, " "e.g. -vvv.") - add("--no-confirm", dest="no_confirm", action="store_true", + auto_group.add_argument("--no-confirm", dest="no_confirm", action="store_true", help="Turn off confirmation screens, currently used for --revoke") - add("-e", "--agree-tos", dest="tos", action="store_true", - help="Skip the end user license agreement screen.") + add("--agree-eula", "-e", dest="tos", action="store_true", + help="Agree to the Let's Encrypt Subscriber Agreement") add("-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") @@ -258,11 +263,11 @@ def create_parser(plugins): "really know what you're doing!") testing_group.add_argument( "--no-verify-ssl", action="store_true", - help=config_help("no_verify_ssl"), + help=config_help("no_verify_ssl",hidden=True), default=flag_default("no_verify_ssl")) # TODO: apache and nginx plugins do NOT respect it testing_group.add_argument( - "--dvsni-port", type=int, help=config_help("dvsni_port"), + "--dvsni-port", type=int, help=config_help("dvsni_port",hidden=True), default=flag_default("dvsni_port")) subparsers = parser.add_subparsers(metavar="SUBCOMMAND") @@ -301,8 +306,8 @@ def create_parser(plugins): add("-d", "--domains", metavar="DOMAIN", action="append") add("-s", "--server", default=flag_default("server"), help=config_help("server")) - add("-k", "--authkey", type=read_file, - help="Path to the authorized key file") + add("-k", "--accountkey", type=read_file, + help="Path to the account key file") add("-m", "--email", help=config_help("email")) add("-B", "--rsa-key-size", type=int, metavar="N", default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) @@ -336,26 +341,27 @@ def create_parser(plugins): def _paths_parser(parser): add = parser.add_argument + hidden = True add("--config-dir", default=flag_default("config_dir"), - help=config_help("config_dir")) + help=config_help("config_dir", hidden)) add("--work-dir", default=flag_default("work_dir"), - help=config_help("work_dir")) + help=config_help("work_dir", hidden)) add("--backup-dir", default=flag_default("backup_dir"), - help=config_help("backup_dir")) + help=config_help("backup_dir", hidden)) add("--key-dir", default=flag_default("key_dir"), - help=config_help("key_dir")) + help=config_help("key_dir", hidden)) add("--cert-dir", default=flag_default("certs_dir"), - help=config_help("cert_dir")) + help=config_help("cert_dir", hidden)) add("--le-vhost-ext", default="-le-ssl.conf", - help=config_help("le_vhost_ext")) + help=config_help("le_vhost_ext", hidden)) add("--cert-path", default=flag_default("cert_path"), - help=config_help("cert_path")) + help=config_help("cert_path", hidden)) add("--chain-path", default=flag_default("chain_path"), - help=config_help("chain_path")) + help=config_help("chain_path", hidden)) add("--renewer-config-file", default=flag_default("renewer_config_file"), - help=config_help("renewer_config_file")) + help=config_help("renewer_config_file", hidden)) return parser diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index dacbe9040..739933bfd 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -23,7 +23,7 @@ CLI_DEFAULTS = dict( chain_path="/etc/letsencrypt/certs/chain-letsencrypt.pem", renewer_config_file="/etc/letsencrypt/renewer.conf", no_verify_ssl=False, - dvsni_port=challenges.DVSNI.PORT, + dvsni_port=443, ) """Defaults for CLI flags and `.IConfig` attributes.""" diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 32bee2b49..241acbcf1 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -1,5 +1,6 @@ """Plugin common functions.""" import zope.interface +import argparse from acme.jose import util as jose_util @@ -55,6 +56,7 @@ class Plugin(object): # dummy function, doesn't check if dest.startswith(self.dest_namespace) def add(arg_name_no_prefix, *args, **kwargs): # pylint: disable=missing-docstring + kwargs["help"] = argparse.SUPPRESS return parser.add_argument( "--{0}{1}".format(option_namespace(name), arg_name_no_prefix), *args, **kwargs) From 1fa5a64abdf3006374a65bab2ad07de29d16ac7f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Jun 2015 01:38:39 -0700 Subject: [PATCH 017/109] Draft (somewhat buggy) implementation of help topics --- letsencrypt/cli.py | 238 ++++++++++++++++++++++++++++++++------------- 1 file changed, 171 insertions(+), 67 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 2f3eabaec..8e5c48a54 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -5,6 +5,7 @@ import atexit import logging import os import sys +import types import configargparse import zope.component @@ -231,46 +232,154 @@ def config_help(name, hidden=False): else: return interfaces.IConfig[name].__doc__ +class SilentParser: + """An a mini parser wrapper that doesn't print help for its + arguments... this one is just needed to the use of callbacks to define + arguments within plugins""" + def __init__(self, parser): + self.parser = parser + def add_argument(self, *args, **kwargs): + kwargs["help"] = argparse.SUPPRESS + self.parser.add_argument(*args, **kwargs) -def create_parser(plugins): +HELP_TOPICS = ["all","security","paths","automation","testing"] +class HelpfulArgumentParser: + """This class wraps argparse, adding the ability to make --help less + verbose, and request help on specific subcategories at a time, eg + 'letsencrypt --help security' for security options.""" + + def __init__(self, args, plugins): + self.args = args + plugin_names = [name for name, p in plugins.iteritems()] + self.help_topics = HELP_TOPICS + plugin_names + self.parser = configargparse.ArgParser( + description=__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + args_for_setting_config_path=["-c", "--config"], + default_config_files=flag_default("config_files")) + self.silent_parser = SilentParser(self.parser) + + h1 = self.prescan_for_flag("-h", self.help_topics) + h2 = self.prescan_for_flag("--h", self.help_topics) + assert max(True,"a") == "a", "Gravity changed direction" + help_arg = max(h1, h2) + self.visible_topics = self.determine_help_topics(help_arg) + print self.visible_topics + self.groups = {} # elements are added by .add_group() + self.add_plugin_args(plugins) + + def prescan_for_flag(self, flag, possible_arguments): + """check for a flag, which accepts a fixed set of possible arguments, in + the command line; we will use this information to configure argparse's + help correctly. Return the flag's argument, if it has one that matches + the sequence @possible_arguments; otherwise return whether the flag is + present""" + if flag not in self.args: + return False + pos = self.args.index(flag) + try: + nxt = self.args[pos + 1] + if nxt in possible_arguments: + return nxt + except: + pass + return True + + def add(self, topic, *args, **kwargs): + """Add a new command line argument. @topic is required, to indicate + which part of the help will document it, but can be None for `always + documented'.""" + + if topic and self.visible_topics[topic]: + g = self.groups[topic] + g.add_argument(*args, **kwargs) + else: + kwargs["help"] = argparse.SUPPRESS + self.parser.add_argument(*args, **kwargs) + + def add_group(self, topic, **kwargs): + """This has to be called once for every topic; but we leave those calls + next to the argument definitions for clarity. Return something + arguments can be added to if necessary, either the parser or an argument + group.""" + if self.visible_topics[topic]: + #print "Adding visible group " + topic + g = self.parser.add_argument_group(topic, **kwargs) + self.groups[topic] = g + #def silencing_shim(self2, *args,**kwargs): + # kwargs["help"] = argparse.SUPPRESS + # self2._real_add_argument(*args, **kwargs) + #g._real_add_argument = g.add_argument + #g.add_argument = types.MethodType(silencing_shim, g) + return g + else: + #print "Invisible group " + topic + # The plugins is going to try to add non-silent arguments; we have + # to stop that... + return self.silent_parser + + def add_plugin_args(self, plugins): + # TODO: plugin_parser should be called for every detected plugin + for name, plugin_ep in plugins.iteritems(): + parser_or_group = self.add_group(name, description=plugin_ep.description) + #print parser_or_group + plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name) + + def determine_help_topics(self, chosen_topic): + """The user may have requested help on a topic, return a dict of which + topics to dislpay. @chosen_topic has prescan_for_flag's return type""" + + # topics maps each topic to whether it should be documented by + # argparse on the command line + if chosen_topic == "all": + return dict([(t,True) for t in self.help_topics]) + elif not chosen_topic: + return dict([(t,False) for t in self.help_topics]) + else: + return dict([(t,t == chosen_topic) for t in self.help_topics]) + + +def create_parser(plugins, args): """Create parser.""" - parser = configargparse.ArgParser( - description=__doc__, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - args_for_setting_config_path=["-c", "--config"], - default_config_files=flag_default("config_files")) - add = parser.add_argument + helpful = HelpfulArgumentParser(args, plugins) - auto_group = parser.add_argument_group( - "auto", description="Arguments for automating execution") - # --help is automatically provided by argparse - auto_group.add_argument("--version", action="version", - version="%(prog)s {0}".format(letsencrypt.__version__)) - add("-v", "--verbose", dest="verbose_count", action="count", + helpful.add( + None, "-v", "--verbose", dest="verbose_count", action="count", default=flag_default("verbose_count"), help="This flag can be used " "multiple times to incrementally increase the verbosity of output, " "e.g. -vvv.") - auto_group.add_argument("--no-confirm", dest="no_confirm", action="store_true", + # --help is automatically provided by argparse + + helpful.add_group( "automation", + description="Arguments for automating execution & other tweaks") + helpful.add( + "automation", "--version", action="version", + 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, currently used for --revoke") - add("--agree-eula", "-e", dest="tos", action="store_true", + helpful.add( + "automation", "--agree-eula", "-e", dest="tos", action="store_true", help="Agree to the Let's Encrypt Subscriber Agreement") - add("-t", "--text", dest="text_mode", action="store_true", + helpful.add( + None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") - testing_group = parser.add_argument_group( + helpful.add_group( "testing", description="The following flags are meant for " "testing purposes only! Do NOT change them, unless you " "really know what you're doing!") - testing_group.add_argument( - "--no-verify-ssl", action="store_true", + helpful.add( + "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl",hidden=True), default=flag_default("no_verify_ssl")) # TODO: apache and nginx plugins do NOT respect it - testing_group.add_argument( - "--dvsni-port", type=int, help=config_help("dvsni_port",hidden=True), - default=flag_default("dvsni_port")) + helpful.add( + "testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"), + help=config_help("dvsni_port",hidden=True)) - subparsers = parser.add_subparsers(metavar="SUBCOMMAND") + 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__) @@ -294,25 +403,27 @@ def create_parser(plugins): "--installers", action="append_const", dest="ifaces", const=interfaces.IInstaller) - parser.add_argument("--configurator") - parser.add_argument("-a", "--authenticator") - parser.add_argument("-i", "--installer") + helpful.add(None, "--configurator") + helpful.add(None, "-a", "--authenticator") + helpful.add(None, "-i", "--installer") # positional arg shadows --domains, instead of appending, and # --domains is useful, because it can be stored in config #for subparser in parser_run, parser_auth, parser_install: # subparser.add_argument("domains", nargs="*", metavar="domain") - add("-d", "--domains", metavar="DOMAIN", action="append") - add("-s", "--server", default=flag_default("server"), - help=config_help("server")) - add("-k", "--accountkey", type=read_file, + helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append") + helpful.add(None, "-k", "--accountkey", type=read_file, help="Path to the account key file") - add("-m", "--email", help=config_help("email")) - add("-B", "--rsa-key-size", type=int, metavar="N", - default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) + helpful.add(None, "-m", "--email", help=config_help("email")) + + helpful.add_group( + "security", description="Security parameters & server settings") + helpful.add("security", "-B", "--rsa-key-size", type=int, metavar="N", + default=flag_default("rsa_key_size"), + help=config_help("rsa_key_size",True)) # TODO: resolve - assumes binary logic while client.py assumes ternary. - add("-r", "--redirect", action="store_true", + helpful.add("security", "-r", "--redirect", action="store_true", help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") @@ -328,49 +439,42 @@ def create_parser(plugins): default=flag_default("rollback_checkpoints"), help="Revert configuration N number of checkpoints.") - _paths_parser(parser.add_argument_group("paths")) - - # TODO: plugin_parser should be called for every detected plugin - for name, plugin_ep in plugins.iteritems(): - plugin_ep.plugin_cls.inject_parser_options( - parser.add_argument_group( - name, description=plugin_ep.description), name) - - return parser + _paths_parser(helpful) -def _paths_parser(parser): - add = parser.add_argument - hidden = True - add("--config-dir", default=flag_default("config_dir"), - help=config_help("config_dir", hidden)) - add("--work-dir", default=flag_default("work_dir"), - help=config_help("work_dir", hidden)) - add("--backup-dir", default=flag_default("backup_dir"), - help=config_help("backup_dir", hidden)) - add("--key-dir", default=flag_default("key_dir"), - help=config_help("key_dir", hidden)) - add("--cert-dir", default=flag_default("certs_dir"), - help=config_help("cert_dir", hidden)) + return helpful.parser - add("--le-vhost-ext", default="-le-ssl.conf", - help=config_help("le_vhost_ext", hidden)) - add("--cert-path", default=flag_default("cert_path"), - help=config_help("cert_path", hidden)) - add("--chain-path", default=flag_default("chain_path"), - help=config_help("chain_path", hidden)) - add("--renewer-config-file", default=flag_default("renewer_config_file"), - help=config_help("renewer_config_file", hidden)) - - return parser +def _paths_parser(helpful): + add = helpful.add + helpful.add_group("paths", description="Arguments changing execution paths & servers") + add("paths", "--config-dir", default=flag_default("config_dir"), + help=config_help("config_dir")) + add("paths", "--work-dir", default=flag_default("work_dir"), + help=config_help("work_dir")) + add("paths", "--backup-dir", default=flag_default("backup_dir"), + help=config_help("backup_dir")) + add("paths", "--key-dir", default=flag_default("key_dir"), + help=config_help("key_dir")) + add("paths", "--cert-dir", default=flag_default("certs_dir"), + help=config_help("cert_dir")) + add("paths", "--le-vhost-ext", default="-le-ssl.conf", + help=config_help("le_vhost_ext")) + add("paths", "--cert-path", default=flag_default("cert_path"), + help=config_help("cert_path")) + add("paths", "--chain-path", default=flag_default("chain_path"), + help=config_help("chain_path")) + add("paths", "--renewer-config-file", default=flag_default("renewer_config_file"), + help=config_help("renewer_config_file")) + add("paths","-s", "--server", default=flag_default("server"), + help=config_help("server")) def main(args=sys.argv[1:]): """Command line argument parsing and main script execution.""" # note: arg parser internally handles --help (and exits afterwards) plugins = plugins_disco.PluginsRegistry.find_all() - args = create_parser(plugins).parse_args(args) + args = create_parser(plugins,args).parse_args(args) config = configuration.NamespaceConfig(args) # Displayer From 2d65026d6dccf50c4abe988e070b6e69cebc1a69 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Jun 2015 11:42:15 -0700 Subject: [PATCH 018/109] Help topics now working --- letsencrypt/cli.py | 8 ++++---- letsencrypt/plugins/common.py | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8e5c48a54..65898c755 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -264,7 +264,7 @@ class HelpfulArgumentParser: assert max(True,"a") == "a", "Gravity changed direction" help_arg = max(h1, h2) self.visible_topics = self.determine_help_topics(help_arg) - print self.visible_topics + #print self.visible_topics self.groups = {} # elements are added by .add_group() self.add_plugin_args(plugins) @@ -372,12 +372,12 @@ def create_parser(plugins, args): "really know what you're doing!") helpful.add( "testing", "--no-verify-ssl", action="store_true", - help=config_help("no_verify_ssl",hidden=True), + help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) # TODO: apache and nginx plugins do NOT respect it helpful.add( "testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"), - help=config_help("dvsni_port",hidden=True)) + help=config_help("dvsni_port")) subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") def add_subparser(name, func): # pylint: disable=missing-docstring @@ -421,7 +421,7 @@ def create_parser(plugins, args): "security", description="Security parameters & server settings") helpful.add("security", "-B", "--rsa-key-size", type=int, metavar="N", default=flag_default("rsa_key_size"), - help=config_help("rsa_key_size",True)) + help=config_help("rsa_key_size")) # TODO: resolve - assumes binary logic while client.py assumes ternary. helpful.add("security", "-r", "--redirect", action="store_true", help="Automatically redirect all HTTP traffic to HTTPS for the newly " diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 241acbcf1..32bee2b49 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -1,6 +1,5 @@ """Plugin common functions.""" import zope.interface -import argparse from acme.jose import util as jose_util @@ -56,7 +55,6 @@ class Plugin(object): # dummy function, doesn't check if dest.startswith(self.dest_namespace) def add(arg_name_no_prefix, *args, **kwargs): # pylint: disable=missing-docstring - kwargs["help"] = argparse.SUPPRESS return parser.add_argument( "--{0}{1}".format(option_namespace(name), arg_name_no_prefix), *args, **kwargs) From c70bce8b7ade2bb06c3f0333307346bf0473775d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 8 Jun 2015 16:51:15 -0700 Subject: [PATCH 019/109] Some cleaning up --- letsencrypt/cli.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 65898c755..d4e416881 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -5,7 +5,6 @@ import atexit import logging import os import sys -import types import configargparse import zope.component @@ -306,16 +305,9 @@ class HelpfulArgumentParser: #print "Adding visible group " + topic g = self.parser.add_argument_group(topic, **kwargs) self.groups[topic] = g - #def silencing_shim(self2, *args,**kwargs): - # kwargs["help"] = argparse.SUPPRESS - # self2._real_add_argument(*args, **kwargs) - #g._real_add_argument = g.add_argument - #g.add_argument = types.MethodType(silencing_shim, g) return g else: #print "Invisible group " + topic - # The plugins is going to try to add non-silent arguments; we have - # to stop that... return self.silent_parser def add_plugin_args(self, plugins): From 887f91bdacc79dabaa92d05af81a57a41c51c00b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 9 Jun 2015 07:38:07 +0000 Subject: [PATCH 020/109] requirements.txt: no editable (-e) mode, no "." install. --- docs/contributing.rst | 2 +- docs/using.rst | 2 +- requirements.txt | 3 +-- tox.ini | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index da28686a2..f527ba421 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -48,7 +48,7 @@ synced to ``/vagrant``, so you can get started with: vagrant ssh cd /vagrant - ./venv/bin/pip install -r requirements.txt + ./venv/bin/pip install -r requirements.txt .[dev,docs,testing] sudo ./venv/bin/letsencrypt Support for other Linux distributions coming soon. diff --git a/docs/using.rst b/docs/using.rst index 89cbc48f6..a93e2240b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -71,7 +71,7 @@ Installation .. code-block:: shell virtualenv --no-site-packages -p python2 venv - ./venv/bin/pip install -r requirements.txt + ./venv/bin/pip install -r requirements.txt . Usage diff --git a/requirements.txt b/requirements.txt index 0f0223dab..972e87eaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ # https://github.com/bw2/ConfigArgParse/issues/17 --e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse --e . +git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse diff --git a/tox.ini b/tox.ini index 0367b5498..aed60f454 100644 --- a/tox.ini +++ b/tox.ini @@ -22,12 +22,12 @@ setenv = [testenv:cover] basepython = python2.7 commands = - pip install -e .[testing] + pip install -r requirements.txt -e .[testing] ./tox.cover.sh [testenv:lint] # recent versions of pylint do not support Python 2.6 (#97, #187) basepython = python2.7 commands = - pip install -e .[dev] + pip install -r requirements.txt -e .[dev] pylint --rcfile=.pylintrc letsencrypt acme letsencrypt_apache letsencrypt_nginx From 22fd9d4cd7b1a1afd3d596086b5b37974abba0e9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 12:28:46 +0000 Subject: [PATCH 021/109] tox.cover.sh: erase coverage before tests --- tox.cover.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.cover.sh b/tox.cover.sh index 80b6474d7..9a2c3f141 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -15,6 +15,8 @@ cover () { "$1" --cover-min-percentage="$2" "$1" } +rm -f .coverage # --cover-erase is off, make sure stats are correct + # don't use sequential composition (;), if letsencrypt_nginx returns # 0, coveralls submit will be triggered (c.f. .travis.yml, # after_success) From ad5c3ff1b239d634e44286e2c58f8fd713c3dea9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:13:31 +0000 Subject: [PATCH 022/109] Support M2Crypto with swig 3.0.5+ Fixes issues recognized in https://github.com/letsencrypt/lets-encrypt-preview/issues/413#issuecomment-106245456 and https://github.com/letsencrypt/lets-encrypt-preview/issues/493. --- requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/requirements.txt b/requirements.txt index 0f0223dab..fa3dba412 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ # https://github.com/bw2/ConfigArgParse/issues/17 -e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse + +# Support swig 3.0.5 +# https://github.com/M2Crypto/M2Crypto/issues/24 +# https://github.com/M2Crypto/M2Crypto/pull/30 +git+https://github.com/M2Crypto/M2Crypto/commit/d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto + -e . From c4b495aa37085601dd7e5bcb94a66cd7d69acbcb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:24:07 +0000 Subject: [PATCH 023/109] Bootstrap Fedora 22 (fixes: #493) --- bootstrap/fedora.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100755 bootstrap/fedora.sh diff --git a/bootstrap/fedora.sh b/bootstrap/fedora.sh new file mode 100755 index 000000000..0b919b4ce --- /dev/null +++ b/bootstrap/fedora.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Tested with: +# - Fedora 22 (x64) + +yum install -y \ + git-core \ + python \ + python-devel \ + python-virtualenv \ + python-devel \ + gcc \ + swig \ + dialog \ + augeas \ + openssl-devel \ + libffi-devel \ + ca-certificates \ From 4ed1a1c2d6b31079b47d8a76adbbd9702fa8405d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:25:49 +0000 Subject: [PATCH 024/109] Bootstrap Debian: one dep per line --- bootstrap/_deb_common.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 4e4c75b33..5348715eb 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -45,5 +45,15 @@ fi apt-get update apt-get install -y --no-install-recommends \ - python python-setuptools "$virtualenv" python-dev gcc swig \ - dialog libaugeas0 libssl-dev libffi-dev ca-certificates dpkg-dev + python \ + python-setuptools \ + "$virtualenv" \ + python-dev \ + gcc \ + swig \ + dialog \ + libaugeas0 \ + libssl-dev \ + libffi-dev \ + ca-certificates \ + dpkg-dev \ From 19aea3720387414747042683c0ee0df2fb80199a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:26:17 +0000 Subject: [PATCH 025/109] Bootstrap Debian: remove python-setuptools dep --- bootstrap/_deb_common.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 5348715eb..7e68c7717 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -46,7 +46,6 @@ fi apt-get update apt-get install -y --no-install-recommends \ python \ - python-setuptools \ "$virtualenv" \ python-dev \ gcc \ From b44014b06e7eb320656e954d2812cb9bf068f9b6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:28:15 +0000 Subject: [PATCH 026/109] Bootstrap README: deps rationale --- bootstrap/README | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bootstrap/README b/bootstrap/README index 847129c03..73564aaab 100644 --- a/bootstrap/README +++ b/bootstrap/README @@ -1,2 +1,7 @@ This directory contains scripts that install necessary OS-specific -prerequisite dependencies (see docs/using.rst). \ No newline at end of file +prerequisite dependencies (see docs/using.rst). + +General dependencies: +- git-core: requirements.txt git+https://* +- ca-certificates: communication with demo ACMO server at + https://www.letsencrypt-demo.org, requirements.txt git+https://* \ No newline at end of file From 2139971212b9e534d6593365fb92a51faff9d37d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:35:29 +0000 Subject: [PATCH 027/109] nit: add EOF newline --- bootstrap/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/README b/bootstrap/README index 73564aaab..6a04ac0ba 100644 --- a/bootstrap/README +++ b/bootstrap/README @@ -4,4 +4,4 @@ prerequisite dependencies (see docs/using.rst). General dependencies: - git-core: requirements.txt git+https://* - ca-certificates: communication with demo ACMO server at - https://www.letsencrypt-demo.org, requirements.txt git+https://* \ No newline at end of file + https://www.letsencrypt-demo.org, requirements.txt git+https://* From 2be914f0d5ed2d3d27e49f679cd57dc7703c4a27 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 21:06:44 +0000 Subject: [PATCH 028/109] Bootstrap Debian: add git-core dep --- bootstrap/_deb_common.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 7e68c7717..653daca53 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -45,9 +45,10 @@ fi apt-get update apt-get install -y --no-install-recommends \ + git-core \ python \ - "$virtualenv" \ python-dev \ + "$virtualenv" \ gcc \ swig \ dialog \ From 30545e1c545fe1798ab6cc961a764798510957e4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 21:11:25 +0000 Subject: [PATCH 029/109] requirements.txt: fix M2Crypto URL --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa3dba412..6c2535a9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ # Support swig 3.0.5 # https://github.com/M2Crypto/M2Crypto/issues/24 # https://github.com/M2Crypto/M2Crypto/pull/30 -git+https://github.com/M2Crypto/M2Crypto/commit/d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto +git+https://github.com/M2Crypto/M2Crypto.git@d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto -e . From 8ba51665637be5e929969e18f19504dd344f294f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 11:54:12 +0000 Subject: [PATCH 030/109] Vagrant: remove explicit git-core install --- Vagrantfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 7eb2b4cce..1d3b48f06 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -8,8 +8,6 @@ VAGRANTFILE_API_VERSION = "2" $ubuntu_setup_script = < Date: Fri, 12 Jun 2015 12:55:33 +0000 Subject: [PATCH 031/109] Separate requirements.txt for SWIG 3.0.5+ --- docs/using.rst | 6 ++++ requirements-swig-3.0.5.txt | 67 +++++++++++++++++++++++++++++++++++++ requirements.txt | 6 ---- 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 requirements-swig-3.0.5.txt diff --git a/docs/using.rst b/docs/using.rst index 89cbc48f6..909c6eadc 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -31,6 +31,12 @@ In general: * ``sudo`` is required as a suggested way of running privileged process * `swig`_ is required for compiling `m2crypto`_ + + .. note:: If your operating system uses SWIG 3.0.5+, you will need + to run ``pip install -r requirements-swig-3.0.5.txt -r + requirements.txt`` instead of the standard ``pip + install -r requirements.txt``. + * `augeas`_ is required for the ``python-augeas`` bindings diff --git a/requirements-swig-3.0.5.txt b/requirements-swig-3.0.5.txt new file mode 100644 index 000000000..9ef45d950 --- /dev/null +++ b/requirements-swig-3.0.5.txt @@ -0,0 +1,67 @@ +# Support swig 3.0.5+ +# https://github.com/M2Crypto/M2Crypto/issues/24 +# https://github.com/M2Crypto/M2Crypto/pull/30 +git+https://github.com/M2Crypto/M2Crypto.git@d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto + +# This requirements file will fail on Travis CI 12.04 LTS Ubuntu build +# machine under TOX_ENV=py26 with very confusing error (full tracback +# at https://api.travis-ci.org/jobs/66529698/log.txt?deansi=true): + +#Traceback (most recent call last): +# File "setup.py", line 133, in +# include_package_data=True, +# File "/opt/python/2.6.9/lib/python2.6/distutils/core.py", line 152, in setup +# dist.run_commands() +# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 975, in run_commands +# self.run_command(cmd) +# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 995, in run_command +# cmd_obj.run() +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 142, in run +# self.with_project_on_sys_path(self.run_tests) +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 122, in with_project_on_sys_path +# func() +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 163, in run_tests +# testRunner=self._resolve_as_ep(self.test_runner), +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 816, in __init__ +# self.parseArgs(argv) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 843, in parseArgs +# self.createTests() +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 849, in createTests +# self.module) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 613, in loadTestsFromNames +# suites = [self.loadTestsFromName(name, module) for name in names] +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 587, in loadTestsFromName +# return self.loadTestsFromModule(obj) +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 37, in loadTestsFromModule +# tests.append(self.loadTestsFromName(submodule)) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 584, in loadTestsFromName +# parent, obj = obj, getattr(obj, part) +#AttributeError: 'module' object has no attribute 'continuity_auth' + +# the above error happens because letsencrypt.continuity_auth cannot import M2Crypto: + +#>>> import M2Crypto +#Traceback (most recent call last): +# File "", line 1, in +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/__init__.py", line 22, in +# import m2crypto +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 26, in +# _m2crypto = swig_import_helper() +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 22, in swig_import_helper +# _mod = imp.load_module('_m2crypto', fp, pathname, description) +#ImportError: /root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/_m2crypto.so: undefined symbol: SSLv2_method + +# For more info see: + +# - https://github.com/martinpaljak/M2Crypto/commit/84977c532c2444c5487db57146d81bb68dd5431d +# - http://stackoverflow.com/questions/10547332/install-m2crypto-on-a-virtualenv-without-system-packages +# - http://stackoverflow.com/questions/8206546/undefined-symbol-sslv2-method + +# In short: Python has been built without SSLv2 support, and +# github.com/M2Crypto/M2Crypto version doesn't contain necessary +# patch, but it's the only one that has a patch for newer versions of +# swig... + +# Problem seems not exists on Python 2.7. It's unlikely that the +# target distribution has swig 3.0.5+ and doesn't have Python 2.7, so +# this file should only be used in conjuction with Python 2.6. diff --git a/requirements.txt b/requirements.txt index 6c2535a9d..0f0223dab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,3 @@ # https://github.com/bw2/ConfigArgParse/issues/17 -e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse - -# Support swig 3.0.5 -# https://github.com/M2Crypto/M2Crypto/issues/24 -# https://github.com/M2Crypto/M2Crypto/pull/30 -git+https://github.com/M2Crypto/M2Crypto.git@d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto - -e . From d585b4468014f9e2ad27a94f1256ad8ddf5a7353 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 12:58:17 +0000 Subject: [PATCH 032/109] Nit: character upper case fixes. --- docs/using.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 909c6eadc..01dda3ce6 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -30,14 +30,14 @@ are provided mainly for the :ref:`developers ` reference. In general: * ``sudo`` is required as a suggested way of running privileged process -* `swig`_ is required for compiling `m2crypto`_ +* `SWIG`_ is required for compiling `M2Crypto`_ .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r requirements.txt`` instead of the standard ``pip install -r requirements.txt``. -* `augeas`_ is required for the ``python-augeas`` bindings +* `Augeas`_ is required for the Python bindings Ubuntu @@ -90,6 +90,6 @@ The letsencrypt commandline tool has a builtin help: ./venv/bin/letsencrypt --help -.. _augeas: http://augeas.net/ -.. _m2crypto: https://github.com/M2Crypto/M2Crypto -.. _swig: http://www.swig.org/ +.. _Augeas: http://augeas.net/ +.. _M2Crypto: https://github.com/M2Crypto/M2Crypto +.. _SWIG: http://www.swig.org/ From ad79d7c8b6471c56c05c8ccccdaaffbbd2f4deb0 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 17:16:04 +0000 Subject: [PATCH 033/109] Adjust client reports to use RenewerConfiguration. Fix docs. --- letsencrypt/client.py | 5 ++--- letsencrypt/configuration.py | 5 ----- letsencrypt/constants.py | 6 +++--- letsencrypt/storage.py | 1 + letsencrypt/tests/client_test.py | 15 +++++++------- letsencrypt/tests/configuration_test.py | 26 ++++++++++++++++++++++--- 6 files changed, 37 insertions(+), 21 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index be89ea8b7..d04116de2 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -236,8 +236,7 @@ class Client(object): # pylint: disable=no-self-use """Informs the user about automatic renewal and deployment. - :param cert: Newly issued certificate - :type cert: :class:`letsencrypt.storage.RenewableCert` + :param .RenewableCert cert: Newly issued certificate """ if ("autorenew" not in cert.configuration @@ -256,7 +255,7 @@ class Client(object): msg += ("been enabled for your certificate. These settings can be " "configured in the directories under {0}.").format( - cert.configuration["renewal_configs_dir"]) + cert.cli_config.renewal_configs_dir) reporter = zope.component.getUtility(interfaces.IReporter) reporter.add_message(msg, reporter.LOW_PRIORITY, True) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 522d697d9..7bd5c2ca4 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -81,11 +81,6 @@ class NamespaceConfig(object): def rec_token_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.REC_TOKEN_DIR) - @property - def renewer_config_file(self): # pylint: disable=missing-docstring - return os.path.join( - self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME) - @property def temp_checkpoint_dir(self): # pylint: disable=missing-docstring return os.path.join( diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index d949c60ec..202871144 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -47,7 +47,7 @@ List of expected options parameters: """ ARCHIVE_DIR = "archive" -"""TODO relative to `IConfig.config_dir`.""" +"""Archive directory, relative to `IConfig.config_dir`.""" CONFIG_DIRS_MODE = 0o755 """Directory mode for ``.IConfig.config_dir`` et al.""" @@ -76,7 +76,7 @@ KEY_DIR = "keys" """Directory (relative to `IConfig.config_dir`) where keys are saved.""" LIVE_DIR = "live" -"""TODO relative to `IConfig.config_dir`.""" +"""Live directory, relative to `IConfig.config_dir`.""" TEMP_CHECKPOINT_DIR = "temp_checkpoint" """Temporary checkpoint directory (relative to `IConfig.work_dir`).""" @@ -86,7 +86,7 @@ REC_TOKEN_DIR = "recovery_tokens" `IConfig.work_dir`).""" RENEWAL_CONFIGS_DIR = "configs" -"""TODO relative to `IConfig.config_dir`.""" +"""Renewal configs directory, relative to `IConfig.config_dir`.""" RENEWER_CONFIG_FILENAME = "renewer.conf" """Renewer config file name (relative to `IConfig.config_dir`).""" diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index c314e3b00..4ad1216e6 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -88,6 +88,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param configobj.ConfigObj config_opts: systemwide defaults for renewal properties not otherwise specified in the individual renewal config file. + :param .RenewerConfiguration cli_config: :raises ValueError: if the configuration file's name didn't end in ".conf", or the file is missing or broken. diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1fb9c2a03..59657b627 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -21,7 +21,8 @@ class ClientTest(unittest.TestCase): """Tests for letsencrypt.client.Client.""" def setUp(self): - self.config = mock.MagicMock(no_verify_ssl=False) + self.config = mock.MagicMock( + no_verify_ssl=False, config_dir="/etc/letsencrypt") # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) @@ -39,7 +40,6 @@ class ClientTest(unittest.TestCase): @mock.patch("letsencrypt.client.zope.component.getUtility") def test_report_new_account(self, mock_zope): # pylint: disable=protected-access - self.config.config_dir = "/usr/bin/coffee" self.account.recovery_token = "ECCENTRIC INVISIBILITY RHINOCEROS" self.account.email = "rhino@jungle.io" @@ -54,32 +54,33 @@ class ClientTest(unittest.TestCase): # pylint: disable=protected-access cert = mock.MagicMock() cert.configuration = configobj.ConfigObj() - cert.configuration["renewal_configs_dir"] = "/etc/letsencrypt/configs" + cert.cli_config = configuration.RenewerConfiguration(self.config) cert.configuration["autorenew"] = "True" cert.configuration["autodeploy"] = "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.configuration["renewal_configs_dir"] in msg) + self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autorenew"] = "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.configuration["renewal_configs_dir"] in msg) + self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autodeploy"] = "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.configuration["renewal_configs_dir"] in msg) + self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autorenew"] = "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) - self.assertTrue(cert.configuration["renewal_configs_dir"] in msg) + self.assertTrue(cert.cli_config.renewal_configs_dir in msg) + class DetermineAccountTest(unittest.TestCase): """Tests for letsencrypt.client.determine_authenticator.""" diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 38fea140a..3dee41d85 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -9,10 +9,10 @@ class NamespaceConfigTest(unittest.TestCase): """Tests for letsencrypt.configuration.NamespaceConfig.""" def setUp(self): - from letsencrypt.configuration import NamespaceConfig self.namespace = mock.MagicMock( config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar', server='https://acme-server.org:443/new') + from letsencrypt.configuration import NamespaceConfig self.config = NamespaceConfig(self.namespace) def test_proxy_getattr(self): @@ -38,7 +38,6 @@ class NamespaceConfigTest(unittest.TestCase): constants.IN_PROGRESS_DIR = '../p' constants.KEY_DIR = 'keys' constants.REC_TOKEN_DIR = '/r' - constants.RENEWER_CONFIG_FILENAME = 'r.conf' constants.TEMP_CHECKPOINT_DIR = 't' self.assertEqual( @@ -53,9 +52,30 @@ class NamespaceConfigTest(unittest.TestCase): self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') self.assertEqual(self.config.key_dir, '/tmp/config/keys') self.assertEqual(self.config.rec_token_dir, '/r') - self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') +class RenewerConfigurationTest(unittest.TestCase): + """Test for letsencrypt.configuration.RenewerConfiguration.""" + + def setUp(self): + self.namespace = mock.MagicMock(config_dir='/tmp/config') + from letsencrypt.configuration import RenewerConfiguration + self.config = RenewerConfiguration(self.namespace) + + @mock.patch('letsencrypt.configuration.constants') + def test_dynamic_dirs(self, constants): + constants.ARCHIVE_DIR = "a" + constants.LIVE_DIR = 'l' + constants.RENEWAL_CONFIGS_DIR = "renewal_configs" + constants.RENEWER_CONFIG_FILENAME = 'r.conf' + + self.assertEqual(self.config.archive_dir, '/tmp/config/a') + self.assertEqual(self.config.live_dir, '/tmp/config/l') + self.assertEqual( + self.config.renewal_configs_dir, '/tmp/config/renewal_configs') + self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') + + if __name__ == '__main__': unittest.main() # pragma: no cover From 896d2be1db884fb675e1abf66d48e3b1cc1fb430 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 14 Jun 2015 10:01:24 +0000 Subject: [PATCH 034/109] SimpleHTTP.tls -> SimpleHTTPResponse.tls bug, MAX_PATH_LEN, good_path, scheme --- acme/challenges.py | 34 +++++++++++++++---- acme/challenges_test.py | 45 +++++++++++++++++--------- letsencrypt/tests/acme_util.py | 8 ++--- letsencrypt/tests/auth_handler_test.py | 8 ++--- 4 files changed, 66 insertions(+), 29 deletions(-) diff --git a/acme/challenges.py b/acme/challenges.py index 26f71a2e3..05dc89fc4 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -46,7 +46,6 @@ class SimpleHTTP(DVChallenge): """ACME "simpleHttp" challenge.""" typ = "simpleHttp" token = jose.Field("token") - tls = jose.Field("tls", default=True, omitempty=True) @ChallengeResponse.register @@ -54,20 +53,43 @@ class SimpleHTTPResponse(ChallengeResponse): """ACME "simpleHttp" challenge response.""" typ = "simpleHttp" path = jose.Field("path") + tls = jose.Field("tls", default=True, omitempty=True) - URI_TEMPLATE = "https://{domain}/.well-known/acme-challenge/{path}" - """URI template for HTTPS server provisioned resource.""" + URI_ROOT_PATH = ".well-known/acme-challenge" + """URI root path for the server provisioned resource.""" + + _URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{path}" + + MAX_PATH_LEN = 25 + """Maximum allowed `path` length.""" + + @property + def good_path(self): + """Is `path` good? + + .. todo:: acme-spec: "The value MUST be comprised entirely of + haracters from the URL-safe alphabet for Base64 encoding + [RFC4648]", base64.b64decode ignores those characters + + """ + return len(self.path) <= 25 + + @property + def scheme(self): + """URL scheme for the provisioned resource.""" + return "https" if self.tls else "http" def uri(self, domain): """Create an URI to the provisioned resource. - Forms an URI to the HTTPS server provisioned resource (containing - :attr:`~SimpleHTTP.token`) by populating the :attr:`URI_TEMPLATE`. + Forms an URI to the HTTPS server provisioned resource + (containing :attr:`~SimpleHTTP.token`). :param str domain: Domain name being verified. """ - return self.URI_TEMPLATE.format(domain=domain, path=self.path) + return self._URI_TEMPLATE.format( + scheme=self.scheme, domain=domain, path=self.path) @Challenge.register diff --git a/acme/challenges_test.py b/acme/challenges_test.py index beeec6f73..4c61c0e3d 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -27,17 +27,8 @@ class SimpleHTTPTest(unittest.TestCase): self.jmsg = { 'type': 'simpleHttp', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA', - 'tls': True, } - def test_no_tls(self): - from acme.challenges import SimpleHTTP - self.assertEqual(SimpleHTTP(token='tok', tls=False).to_json(), { - 'tls': False, - 'token': 'tok', - 'type': 'simpleHttp', - }) - def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -54,27 +45,51 @@ class SimpleHTTPResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import SimpleHTTPResponse - self.msg = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA') - self.jmsg = { + self.msg_http = SimpleHTTPResponse( + path='6tbIMBC5Anhl5bOlWT5ZFA', tls=False) + self.msg_https = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA') + self.jmsg_http = { 'type': 'simpleHttp', 'path': '6tbIMBC5Anhl5bOlWT5ZFA', + 'tls': False, + } + self.jmsg_https = { + 'type': 'simpleHttp', + 'path': '6tbIMBC5Anhl5bOlWT5ZFA', + 'tls': True, } + def test_good_path(self): + self.assertTrue(self.msg_http.good_path) + self.assertTrue(self.msg_https.good_path) + self.assertFalse( + self.msg_http.update(path=(self.msg_http.path * 10)).good_path) + + def test_scheme(self): + self.assertEqual('http', self.msg_http.scheme) + self.assertEqual('https', self.msg_https.scheme) + def test_uri(self): + self.assertEqual('http://example.com/.well-known/acme-challenge/' + '6tbIMBC5Anhl5bOlWT5ZFA', self.msg_http.uri('example.com')) self.assertEqual('https://example.com/.well-known/acme-challenge/' - '6tbIMBC5Anhl5bOlWT5ZFA', self.msg.uri('example.com')) + '6tbIMBC5Anhl5bOlWT5ZFA', self.msg_https.uri('example.com')) def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) + self.assertEqual(self.jmsg_http, self.msg_http.to_partial_json()) + self.assertEqual(self.jmsg_https, self.msg_https.to_partial_json()) def test_from_json(self): from acme.challenges import SimpleHTTPResponse self.assertEqual( - self.msg, SimpleHTTPResponse.from_json(self.jmsg)) + self.msg_http, SimpleHTTPResponse.from_json(self.jmsg_http)) + self.assertEqual( + self.msg_https, SimpleHTTPResponse.from_json(self.jmsg_https)) def test_from_json_hashable(self): from acme.challenges import SimpleHTTPResponse - hash(SimpleHTTPResponse.from_json(self.jmsg)) + hash(SimpleHTTPResponse.from_json(self.jmsg_http)) + hash(SimpleHTTPResponse.from_json(self.jmsg_https)) class DVSNITest(unittest.TestCase): diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 51bb3cfbb..daf651059 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -16,7 +16,7 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( "acme.jose", os.path.join("testdata", "rsa512_key.pem")))) # Challenges -SIMPLE_HTTPS = challenges.SimpleHTTP( +SIMPLE_HTTP = challenges.SimpleHTTP( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") DVSNI = challenges.DVSNI( r="O*\xb4-\xad\xec\x95>\xed\xa9\r0\x94\xe8\x97\x9c&6\xbf'\xb3" @@ -47,7 +47,7 @@ POP = challenges.ProofOfPossession( ) ) -CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP] +CHALLENGES = [SIMPLE_HTTP, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES @@ -86,13 +86,13 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages2.STATUS_PENDING) -SIMPLE_HTTPS_P = chall_to_challb(SIMPLE_HTTPS, messages2.STATUS_PENDING) +SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages2.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages2.STATUS_PENDING) RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages2.STATUS_PENDING) RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages2.STATUS_PENDING) POP_P = chall_to_challb(POP, messages2.STATUS_PENDING) -CHALLENGES_P = [SIMPLE_HTTPS_P, DVSNI_P, DNS_P, +CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P] DV_CHALLENGES_P = [challb for challb in CHALLENGES_P if isinstance(challb.chall, challenges.DVChallenge)] diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index d7fd2c093..8cbc0e604 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -300,7 +300,7 @@ class GenChallengePathTest(unittest.TestCase): def test_common_case(self): """Given DVSNI and SimpleHTTP with appropriate combos.""" - challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTPS_P) + challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P) prefs = [challenges.DVSNI] combos = ((0,), (1,)) @@ -315,7 +315,7 @@ class GenChallengePathTest(unittest.TestCase): challbs = (acme_util.RECOVERY_TOKEN_P, acme_util.RECOVERY_CONTACT_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTPS_P) + acme_util.SIMPLE_HTTP_P) prefs = [challenges.RecoveryToken, challenges.DVSNI] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) @@ -328,7 +328,7 @@ class GenChallengePathTest(unittest.TestCase): acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTPS_P, + acme_util.SIMPLE_HTTP_P, acme_util.DNS_P) # Typical webserver client that can do everything except DNS # Attempted to make the order realistic @@ -413,7 +413,7 @@ class IsPreferredTest(unittest.TestCase): def test_mutually_exclusvie(self): self.assertFalse( self._call( - acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTPS_P]))) + acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTP_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( From d4b9499e2b6f35335e56d512688a23c85763c514 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 14 Jun 2015 09:46:13 +0000 Subject: [PATCH 035/109] ManualAuthenticator for SimpleHTTP. Inspired by quite popular [1] letsencrypt-nosudo [2] by @diafygi. Together with #440 and #473, it allows Let's Encrypt to be used without sudo (root) on the target machine (c.f. [3]). Possibly fixes #500. [1] https://news.ycombinator.com/item?id=9707170 [2] https://github.com/diafygi/letsencrypt-nosudo [3] https://groups.google.com/a/letsencrypt.org/forum/#!topic/client-dev/JAqxSvXlln4 --- docs/api/plugins/manual.rst | 5 ++ letsencrypt/plugins/manual.py | 138 +++++++++++++++++++++++++++++ letsencrypt/plugins/manual_test.py | 59 ++++++++++++ setup.py | 1 + 4 files changed, 203 insertions(+) create mode 100644 docs/api/plugins/manual.rst create mode 100644 letsencrypt/plugins/manual.py create mode 100644 letsencrypt/plugins/manual_test.py diff --git a/docs/api/plugins/manual.rst b/docs/api/plugins/manual.rst new file mode 100644 index 000000000..4661ab7df --- /dev/null +++ b/docs/api/plugins/manual.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt.plugins.manual` +--------------------------------- + +.. automodule:: letsencrypt.plugins.manual + :members: diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py new file mode 100644 index 000000000..c4c7b890a --- /dev/null +++ b/letsencrypt/plugins/manual.py @@ -0,0 +1,138 @@ +"""Manual plugin.""" +import logging +import os +import sys + +import requests +import zope.component +import zope.interface + +from acme import challenges +from acme import jose + +from letsencrypt import interfaces +from letsencrypt.plugins import common + + +class ManualAuthenticator(common.Plugin): + """Manual Authenticator. + + .. todo:: Support for `~.challenges.DVSNI`. + + """ + zope.interface.implements(interfaces.IAuthenticator) + zope.interface.classProvides(interfaces.IPluginFactory) + + description = "Manual Authenticator" + + MESSAGE_TEMPLATE = """\ +Make sure your web server displays the following content at +{uri} before continuing: + +{achall.token} + +If you don't have HTTP server configured, you can run the following +command on the target server (as root): + +{command} +""" + + HTTP_TEMPLATE = """\ +mkdir -p {response.URI_ROOT_PATH} +echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} +# run only once per server: +python -m SimpleHTTPServer 80""" + """Non-TLS command template.""" + + # https://www.piware.de/2011/01/creating-an-https-server-in-python/ + HTTPS_TEMPLATE = """\ +mkdir -p {response.URI_ROOT_PATH} # run only once per server +echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} +# run only once per server: +openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout key.pem -out cert.pem +python -c "import BaseHTTPServer, SimpleHTTPServer, ssl; \\ +s = BaseHTTPServer.HTTPServer(('', 443), 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. For the same reason + ``requests.get`` in `_verify` sets ``verify=False``. Python HTTPS + server command serves the ``token`` on all URIs. + + """ + + def __init__(self, *args, **kwargs): + super(ManualAuthenticator, self).__init__(*args, **kwargs) + self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls + else self.HTTPS_TEMPLATE) + + def prepare(self): # pylint: disable=missing-docstring,no-self-use + pass # pragma: no cover + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return """\ +This plugin requires user's manual intervention in setting up a HTTP +server for solving SimpleHTTP challenges and thus does not need to be +run as a privilidged process. Alternatively shows instructions on how +to use Python's built-in HTTP server and, in case of HTTPS, openssl +binary for temporary key/certificate generation.""".replace("\n", "") + + def get_chall_pref(self, domain): + # pylint: disable=missing-docstring,no-self-use,unused-argument + return [challenges.SimpleHTTP] + + def perform(self, achalls): # pylint: disable=missing-docstring + responses = [] + # TODO: group achalls by the same socket.gethostbyname(_ex) + # and prompt only once per server (one "echo -n" per domain) + for achall in achalls: + responses.append(self._perform_single(achall)) + return responses + + def _perform_single(self, achall): + # same path for each challenge response would be easier for + # users, but will not work if multiple domains point at the + # same server: default command doesn't support virtual hosts + response = challenges.SimpleHTTPResponse( + path=jose.b64encode(os.urandom(18)), + tls=(not self.config.no_simple_http_tls)) + assert response.good_path # is encoded os.urandom(18) good? + + self._notify_and_wait(self.MESSAGE_TEMPLATE.format( + achall=achall, response=response, + uri=response.uri(achall.domain), + command=self.template.format(achall=achall, response=response))) + + if self._verify(achall, response): + return response + else: + return None + + def _notify_and_wait(self, message): # pylint: disable=no-self-use + # TODO: IDisplay wraps messages, breaking the command + #answer = zope.component.getUtility(interfaces.IDisplay).notification( + # message=message, height=25, pause=True) + sys.stdout.write(message) + raw_input("Press ENTER to continue") + + def _verify(self, achall, chall_response): # pylint: disable=no-self-use + uri = chall_response.uri(achall.domain) + logging.debug("Verifying %s...", uri) + try: + response = requests.get(uri, verify=False) + except requests.exceptions.ConnectionError as error: + logging.exception(error) + return False + + ret = response.text == achall.token + if not ret: + logging.error("Unable to verify %s! Expected: %r, returned: %r.", + uri, achall.token, response.text) + + return ret + + def cleanup(self, achalls): # pylint: disable=missing-docstring,no-self-use + pass # pragma: no cover diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py new file mode 100644 index 000000000..c95654dec --- /dev/null +++ b/letsencrypt/plugins/manual_test.py @@ -0,0 +1,59 @@ +"""Tests for letsencrypt.plugins.manual.""" +import unittest + +import mock +import requests + +from acme import challenges + +from letsencrypt import achallenges +from letsencrypt.tests import acme_util + + +class ManualAuthenticatorTest(unittest.TestCase): + """Tests for letsencrypt.plugins.manual.ManualAuthenticator.""" + + def setUp(self): + from letsencrypt.plugins.manual import ManualAuthenticator + self.config = mock.MagicMock(no_simple_http_tls=True) + self.auth = ManualAuthenticator(config=self.config, name="manual") + self.achalls = [achallenges.SimpleHTTP( + challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)] + + def test_more_info(self): + self.assertTrue(isinstance(self.auth.more_info(), str)) + + def test_get_chall_pref(self): + self.assertTrue(all(issubclass(pref, challenges.Challenge) + for pref in self.auth.get_chall_pref("foo.com"))) + + def test_perform_empty(self): + self.assertEqual([], self.auth.perform([])) + + @mock.patch("letsencrypt.plugins.manual.sys.stdout") + @mock.patch("letsencrypt.plugins.manual.os.urandom") + @mock.patch("letsencrypt.plugins.manual.requests.get") + @mock.patch("__builtin__.raw_input") + def test_perform(self, mock_raw_input, mock_get, mock_urandom, mock_stdout): + mock_urandom.return_value = "foo" + mock_get().text = self.achalls[0].token + + self.assertEqual( + [challenges.SimpleHTTPResponse(tls=False, path='Zm9v')], + self.auth.perform(self.achalls)) + mock_raw_input.assert_called_once() + mock_get.assert_called_with( + "http://foo.com/.well-known/acme-challenge/Zm9v", verify=False) + + message = mock_stdout.write.mock_calls[0][1][0] + self.assertTrue(self.achalls[0].token in message) + self.assertTrue('Zm9v' in message) + + mock_get().text = self.achalls[0].token + '!' + self.assertEqual([None], self.auth.perform(self.achalls)) + + mock_get.side_effect = requests.exceptions.ConnectionError + self.assertEqual([None], self.auth.perform(self.achalls)) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/setup.py b/setup.py index 145b75a69..16aafac94 100644 --- a/setup.py +++ b/setup.py @@ -120,6 +120,7 @@ setup( 'jws = letsencrypt.acme.jose.jws:CLI.run', ], 'letsencrypt.plugins': [ + 'manual = letsencrypt.plugins.manual:ManualAuthenticator', 'standalone = letsencrypt.plugins.standalone.authenticator' ':StandaloneAuthenticator', From 8dc9cc67d96691cbbaad43dfeef9b7bab5a1f116 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 16 Jun 2015 12:46:37 -0700 Subject: [PATCH 036/109] Satiate the pylint daemons --- letsencrypt/cli.py | 54 ++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index d4e416881..e1c563a48 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -231,25 +231,26 @@ def config_help(name, hidden=False): else: return interfaces.IConfig[name].__doc__ -class SilentParser: +class SilentParser(object): """An a mini parser wrapper that doesn't print help for its arguments... this one is just needed to the use of callbacks to define arguments within plugins""" def __init__(self, parser): self.parser = parser def add_argument(self, *args, **kwargs): + """Wrap, but silence help""" kwargs["help"] = argparse.SUPPRESS self.parser.add_argument(*args, **kwargs) -HELP_TOPICS = ["all","security","paths","automation","testing"] -class HelpfulArgumentParser: +HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] +class HelpfulArgumentParser(object): """This class wraps argparse, adding the ability to make --help less verbose, and request help on specific subcategories at a time, eg 'letsencrypt --help security' for security options.""" def __init__(self, args, plugins): self.args = args - plugin_names = [name for name, p in plugins.iteritems()] + plugin_names = [name for name, _p in plugins.iteritems()] self.help_topics = HELP_TOPICS + plugin_names self.parser = configargparse.ArgParser( description=__doc__, @@ -259,8 +260,8 @@ class HelpfulArgumentParser: self.silent_parser = SilentParser(self.parser) h1 = self.prescan_for_flag("-h", self.help_topics) - h2 = self.prescan_for_flag("--h", self.help_topics) - assert max(True,"a") == "a", "Gravity changed direction" + h2 = self.prescan_for_flag("--help", self.help_topics) + assert max(True, "a") == "a", "Gravity changed direction" help_arg = max(h1, h2) self.visible_topics = self.determine_help_topics(help_arg) #print self.visible_topics @@ -276,11 +277,11 @@ class HelpfulArgumentParser: if flag not in self.args: return False pos = self.args.index(flag) - try: + try: nxt = self.args[pos + 1] if nxt in possible_arguments: return nxt - except: + except IndexError: pass return True @@ -288,10 +289,10 @@ class HelpfulArgumentParser: """Add a new command line argument. @topic is required, to indicate which part of the help will document it, but can be None for `always documented'.""" - + if topic and self.visible_topics[topic]: - g = self.groups[topic] - g.add_argument(*args, **kwargs) + group = self.groups[topic] + group.add_argument(*args, **kwargs) else: kwargs["help"] = argparse.SUPPRESS self.parser.add_argument(*args, **kwargs) @@ -299,18 +300,20 @@ class HelpfulArgumentParser: def add_group(self, topic, **kwargs): """This has to be called once for every topic; but we leave those calls next to the argument definitions for clarity. Return something - arguments can be added to if necessary, either the parser or an argument + arguments can be added to if necessary, either the parser or an argument group.""" if self.visible_topics[topic]: - #print "Adding visible group " + topic - g = self.parser.add_argument_group(topic, **kwargs) - self.groups[topic] = g - return g + #print "Adding visible group " + topic + group = self.parser.add_argument_group(topic, **kwargs) + self.groups[topic] = group + return group else: - #print "Invisible group " + topic + #print "Invisible group " + topic return self.silent_parser def add_plugin_args(self, plugins): + """Let each of the plugins add its own command line arguments, which + may or may not be displayed as help topics.""" # TODO: plugin_parser should be called for every detected plugin for name, plugin_ep in plugins.iteritems(): parser_or_group = self.add_group(name, description=plugin_ep.description) @@ -324,11 +327,11 @@ class HelpfulArgumentParser: # topics maps each topic to whether it should be documented by # argparse on the command line if chosen_topic == "all": - return dict([(t,True) for t in self.help_topics]) + return dict([(t, True) for t in self.help_topics]) elif not chosen_topic: - return dict([(t,False) for t in self.help_topics]) + return dict([(t, False) for t in self.help_topics]) else: - return dict([(t,t == chosen_topic) for t in self.help_topics]) + return dict([(t, t == chosen_topic) for t in self.help_topics]) def create_parser(plugins, args): @@ -342,10 +345,10 @@ def create_parser(plugins, args): "e.g. -vvv.") # --help is automatically provided by argparse - helpful.add_group( "automation", + helpful.add_group("automation", description="Arguments for automating execution & other tweaks") helpful.add( - "automation", "--version", action="version", + "automation", "--version", action="version", version="%(prog)s {0}".format(letsencrypt.__version__), help="show program's version number and exit") helpful.add( @@ -363,7 +366,7 @@ def create_parser(plugins, args): "testing purposes only! Do NOT change them, unless you " "really know what you're doing!") helpful.add( - "testing", "--no-verify-ssl", action="store_true", + "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) # TODO: apache and nginx plugins do NOT respect it @@ -458,15 +461,14 @@ def _paths_parser(helpful): help=config_help("chain_path")) add("paths", "--renewer-config-file", default=flag_default("renewer_config_file"), help=config_help("renewer_config_file")) - - add("paths","-s", "--server", default=flag_default("server"), + add("paths", "-s", "--server", default=flag_default("server"), help=config_help("server")) def main(args=sys.argv[1:]): """Command line argument parsing and main script execution.""" # note: arg parser internally handles --help (and exits afterwards) plugins = plugins_disco.PluginsRegistry.find_all() - args = create_parser(plugins,args).parse_args(args) + args = create_parser(plugins, args).parse_args(args) config = configuration.NamespaceConfig(args) # Displayer From 88a03afe7bda341fcf8bed8a4f07d468687a3824 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 16 Jun 2015 12:47:07 -0700 Subject: [PATCH 037/109] Prevent pylint from complaining about some silly things --- .pylintrc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index 5302133ad..825699036 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 +disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods # abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1) @@ -101,7 +101,7 @@ function-rgx=[a-z_][a-z0-9_]{2,40}$ function-name-hint=[a-z_][a-z0-9_]{2,40}$ # Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ +variable-rgx=[a-z_][a-z0-9_]{1,30}$ # Naming hint for variable names variable-name-hint=[a-z_][a-z0-9_]{2,30}$ @@ -228,7 +228,8 @@ max-module-lines=1250 indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 +# This does something silly/broken... +#indent-after-paren=4 [TYPECHECK] From 40871d4c29f46dd4a001a9e5f8cb24e11bba27be Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 16 Jun 2015 14:04:41 -0700 Subject: [PATCH 038/109] Fix merge error --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 4461d2c02..a48336fe7 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -362,7 +362,7 @@ def create_parser(plugins, args): help="Use the text output instead of the curses UI.") - testing_group = parser.add_argument_group( + helpful.add_group( "testing", description="The following flags are meant for " "testing purposes only! Do NOT change them, unless you " "really know what you're doing!") From 52e20769bb9383938b3bc84caeaec114d3aae388 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 16 Jun 2015 14:19:19 -0700 Subject: [PATCH 039/109] Revert workaround for #482 --- letsencrypt/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index ae783d6bc..47539615d 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -23,7 +23,7 @@ CLI_DEFAULTS = dict( chain_path="/etc/letsencrypt/certs/chain-letsencrypt.pem", renewer_config_file="/etc/letsencrypt/renewer.conf", no_verify_ssl=False, - dvsni_port=443, + dvsni_port=challenges.DVSNI.PORT, ) """Defaults for CLI flags and `.IConfig` attributes.""" From 63d5273ed1692cd4c02af9e0e421ff362b2d06b9 Mon Sep 17 00:00:00 2001 From: PatrickHeppler Date: Thu, 18 Jun 2015 14:55:12 +0200 Subject: [PATCH 040/109] Create centos.sh --- bootstrap/centos.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 bootstrap/centos.sh diff --git a/bootstrap/centos.sh b/bootstrap/centos.sh new file mode 100644 index 000000000..318a47a8a --- /dev/null +++ b/bootstrap/centos.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# Tested with: Centos 7 on AWS EC2 t2.micro (x64) + +yum install -y \ + git \ + python \ + python-devel \ + python-virtualenv \ + python-devel \ + gcc \ + swig \ + dialog \ + augeas-libs \ + openssl-devel \ + libffi-devel \ + ca-certificates \ + python-setuptools \ + readline-devel From 8d0334d2de9f355d867210be01f74f25c6c00e2c Mon Sep 17 00:00:00 2001 From: PatrickHeppler Date: Thu, 18 Jun 2015 14:58:55 +0200 Subject: [PATCH 041/109] Update using.rst Additional informations about installing on Centos 7 --- docs/using.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 89cbc48f6..ddfc67738 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -64,6 +64,11 @@ Mac OSX sudo ./bootstrap/mac.sh +Centos 7 +-------- +.. code-block:: shell + + sudo ./bootstrap/centos.sh Installation ============ @@ -73,6 +78,13 @@ Installation virtualenv --no-site-packages -p python2 venv ./venv/bin/pip install -r requirements.txt +Installation on Centos 7 +============ + +.. code-block:: shell + + virtualenv --no-site-packages -p python2 venv + env SWIG_FEATURES="-cpperraswarn -includeall -D__`uname -m`__ -I/usr/include/openssl" ./venv/bin/python setup.py install Usage ===== From 85d9047f4efb9ef6058ab4a4b55222f55ac7be16 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 13:13:29 +0000 Subject: [PATCH 042/109] Fedora: augeas -> augeas-libs --- bootstrap/fedora.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/fedora.sh b/bootstrap/fedora.sh index 0b919b4ce..97ea1e637 100755 --- a/bootstrap/fedora.sh +++ b/bootstrap/fedora.sh @@ -12,7 +12,7 @@ yum install -y \ gcc \ swig \ dialog \ - augeas \ + augeas-libs \ openssl-devel \ libffi-devel \ ca-certificates \ From 9b4cff8cd60c0d8e4d046ff1c05ccff2db0d0a44 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 13:55:32 +0000 Subject: [PATCH 043/109] bootstrap: _rpm_common.sh, centos fixes --- bootstrap/_rpm_common.sh | 20 ++++++++++++++++++++ bootstrap/centos.sh | 20 +------------------- bootstrap/fedora.sh | 19 +------------------ docs/using.rst | 18 +++++++++++------- 4 files changed, 33 insertions(+), 44 deletions(-) create mode 100755 bootstrap/_rpm_common.sh mode change 100644 => 120000 bootstrap/centos.sh mode change 100755 => 120000 bootstrap/fedora.sh diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh new file mode 100755 index 000000000..1209cd44a --- /dev/null +++ b/bootstrap/_rpm_common.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Tested with: +# - Fedora 22 (x64) +# - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet) + +# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails) +yum install -y \ + git-core \ + python \ + python-devel \ + python-virtualenv \ + python-devel \ + gcc \ + swig \ + dialog \ + augeas-libs \ + openssl-devel \ + libffi-devel \ + ca-certificates \ diff --git a/bootstrap/centos.sh b/bootstrap/centos.sh deleted file mode 100644 index 318a47a8a..000000000 --- a/bootstrap/centos.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -# Tested with: Centos 7 on AWS EC2 t2.micro (x64) - -yum install -y \ - git \ - python \ - python-devel \ - python-virtualenv \ - python-devel \ - gcc \ - swig \ - dialog \ - augeas-libs \ - openssl-devel \ - libffi-devel \ - ca-certificates \ - python-setuptools \ - readline-devel diff --git a/bootstrap/centos.sh b/bootstrap/centos.sh new file mode 120000 index 000000000..a0db46d70 --- /dev/null +++ b/bootstrap/centos.sh @@ -0,0 +1 @@ +_rpm_common.sh \ No newline at end of file diff --git a/bootstrap/fedora.sh b/bootstrap/fedora.sh deleted file mode 100755 index 97ea1e637..000000000 --- a/bootstrap/fedora.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# Tested with: -# - Fedora 22 (x64) - -yum install -y \ - git-core \ - python \ - python-devel \ - python-virtualenv \ - python-devel \ - gcc \ - swig \ - dialog \ - augeas-libs \ - openssl-devel \ - libffi-devel \ - ca-certificates \ diff --git a/bootstrap/fedora.sh b/bootstrap/fedora.sh new file mode 120000 index 000000000..a0db46d70 --- /dev/null +++ b/bootstrap/fedora.sh @@ -0,0 +1 @@ +_rpm_common.sh \ No newline at end of file diff --git a/docs/using.rst b/docs/using.rst index 0e0b493ca..ef64452e1 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -70,12 +70,23 @@ Mac OSX sudo ./bootstrap/mac.sh + Centos 7 -------- + .. code-block:: shell sudo ./bootstrap/centos.sh +For installation run this modified command (note the trailing +backslash): + +.. code-block:: shell + + SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ + ./venv/bin/pip install -r requirements.txt functools32 + + Installation ============ @@ -84,13 +95,6 @@ Installation virtualenv --no-site-packages -p python2 venv ./venv/bin/pip install -r requirements.txt -Installation on Centos 7 -============ - -.. code-block:: shell - - virtualenv --no-site-packages -p python2 venv - env SWIG_FEATURES="-cpperraswarn -includeall -D__`uname -m`__ -I/usr/include/openssl" ./venv/bin/python setup.py install Usage ===== From db6f9ecf862f0175a02cb8d46108ab2f8ec7cc9a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 13:59:59 +0000 Subject: [PATCH 044/109] Fedora installation instructions. --- docs/using.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index ef64452e1..227fd69ed 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -32,6 +32,7 @@ In general: * ``sudo`` is required as a suggested way of running privileged process * `swig`_ is required for compiling `m2crypto`_ + .. _new-swig: .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r requirements.txt`` instead of the standard ``pip @@ -71,6 +72,16 @@ Mac OSX sudo ./bootstrap/mac.sh +Fedora +------ + +.. code-block:: shell + + sudo ./bootstrap/fedora.sh + +.. note:: Fedora 22 uses SWIG 3.0.5+, use the :ref:`modified pip + command for installation `. + Centos 7 -------- From a873e8ea33ec4c6de298aadfe8699d555e56f3ba Mon Sep 17 00:00:00 2001 From: William Budington Date: Thu, 18 Jun 2015 17:45:04 -0700 Subject: [PATCH 045/109] functools32 required - add to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 145b75a69..ebcc2c9b3 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ install_requires = [ # order of items in install_requires DOES matter and M2Crypto has # to go last, see #152 'M2Crypto', + 'functools32' ] dev_extras = [ From b1bb5a6843618dfdc1480a4328d9ff9cdf17b468 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 18 Jun 2015 18:02:51 -0700 Subject: [PATCH 046/109] Make sure cleanup_challenges happens --- letsencrypt/auth_handler.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 5f9d29e6e..5665fe83d 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -134,9 +134,11 @@ class AuthHandler(object): self._send_responses(self.cont_c, cont_resp, chall_update)) # Check for updated status... - self._poll_challenges(chall_update, best_effort) - # This removes challenges from self.dv_c and self.cont_c - self._cleanup_challenges(active_achalls) + try: + self._poll_challenges(chall_update, best_effort) + finally: + # This removes challenges from self.dv_c and self.cont_c + self._cleanup_challenges(active_achalls) def _send_responses(self, achalls, resps, chall_update): """Send responses and make sure errors are handled. From dee1b7f04921aa5b6e193f20681ef1883db09046 Mon Sep 17 00:00:00 2001 From: William Budington Date: Thu, 18 Jun 2015 18:35:35 -0700 Subject: [PATCH 047/109] Remove support for python2.6 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 167d6ad74..9169a32d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ after_success: '[ "$TOXENV" == "cover" ] && coveralls' # matrix, which allows us to clearly distinguish which component under # test has failed env: - - TOXENV=py26 - TOXENV=py27 - TOXENV=lint - TOXENV=cover From 8afc26a7362ab6aa7705125eb1da4780e7a9967d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 04:10:51 +0000 Subject: [PATCH 048/109] Fix typo --- acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/challenges.py b/acme/challenges.py index 05dc89fc4..9ea06645d 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -68,7 +68,7 @@ class SimpleHTTPResponse(ChallengeResponse): """Is `path` good? .. todo:: acme-spec: "The value MUST be comprised entirely of - haracters from the URL-safe alphabet for Base64 encoding + characters from the URL-safe alphabet for Base64 encoding [RFC4648]", base64.b64decode ignores those characters """ From b3be239061e1e2015987487b46f77382038209ad Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 08:14:25 +0000 Subject: [PATCH 049/109] Fix merge conflicts between #486 and #510 (pip install .). --- docs/using.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index a180a3826..bb27ad8c2 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -35,8 +35,8 @@ In general: .. _new-swig: .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r - requirements.txt`` instead of the standard ``pip - install -r requirements.txt``. + requirements.txt .`` instead of the standard ``pip + install -r requirements.txt .``. * `Augeas`_ is required for the Python bindings @@ -95,7 +95,7 @@ backslash): .. code-block:: shell SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ - ./venv/bin/pip install -r requirements.txt functools32 + ./venv/bin/pip install -r requirements.txt functools32 . Installation From 5f41c9f191022ff03b7d4605b000fecee60689d9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 08:16:03 +0000 Subject: [PATCH 050/109] Dockerfile: note about missing requirements.txt --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index b6a07388c..78aa7a75b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,7 @@ COPY letsencrypt_apache /opt/letsencrypt/src/letsencrypt_apache/ COPY letsencrypt_nginx /opt/letsencrypt/src/letsencrypt_nginx/ +# requirements.txt not installed! RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ /opt/letsencrypt/venv/bin/pip install -e /opt/letsencrypt/src From ed7ba282119cdb592b1e9e11d2d51bc2adbda7a8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 07:36:45 +0000 Subject: [PATCH 051/109] Pin jsonschema (quickfix for missing functools32). https://github.com/Julian/jsonschema/issues/233 --- docs/using.rst | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index bb27ad8c2..e377e74ab 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -95,7 +95,7 @@ backslash): .. code-block:: shell SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ - ./venv/bin/pip install -r requirements.txt functools32 . + ./venv/bin/pip install -r requirements.txt . Installation diff --git a/setup.py b/setup.py index ebcc2c9b3..46f53244c 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ install_requires = [ 'argparse', 'ConfigArgParse', 'configobj', - 'jsonschema', + 'jsonschema<2.5.1', # https://github.com/Julian/jsonschema/issues/233 'mock', 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'parsedatetime', @@ -53,7 +53,6 @@ install_requires = [ # order of items in install_requires DOES matter and M2Crypto has # to go last, see #152 'M2Crypto', - 'functools32' ] dev_extras = [ From 6d2c81138e5ffd4d0102ec0595a0184d6063e109 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 07:38:05 +0000 Subject: [PATCH 052/109] Revert "Remove support for python2.6" This reverts commit dee1b7f04921aa5b6e193f20681ef1883db09046. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9169a32d7..167d6ad74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ after_success: '[ "$TOXENV" == "cover" ] && coveralls' # matrix, which allows us to clearly distinguish which component under # test has failed env: + - TOXENV=py26 - TOXENV=py27 - TOXENV=lint - TOXENV=cover From dbd024f77cc6dfba1213fb893ae2f78943ad5f6b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:07:18 +0000 Subject: [PATCH 053/109] Inline docs fixes --- letsencrypt/network2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index 9b846da6c..f2620b4b1 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -25,7 +25,7 @@ class Network(object): .. todo:: Clean up raised error types hierarchy, document, and handle (wrap) - instances of `.DeserializationError` raised in `from_json()``. + instances of `.DeserializationError` raised in `from_json()`. :ivar str new_reg_uri: Location of new-reg :ivar key: `.JWK` (private) From f46e2aeedd69ebe62481f750eb190471b1975676 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:06:39 +0000 Subject: [PATCH 054/109] README: documentation link at the top. Hopefully helps to mitigate problems mentioned in https://groups.google.com/a/letsencrypt.org/forum/#!topic/client-dev/4xpVpy4EVz0 --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index db32889db..5b149abe5 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,9 @@ +.. notice for github users + +Official **documentation**, including `installation instructions`_, is +available at https://letsencrypt.readthedocs.org. + + About the Let's Encrypt Client ============================== @@ -47,6 +53,9 @@ server automatically!:: :target: https://quay.io/repository/letsencrypt/lets-encrypt-preview :alt: Docker Repository on Quay.io +.. _`installation instructions`: + https://letsencrypt.readthedocs.org/en/latest/using.html + .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU @@ -85,7 +94,7 @@ Current Features Links ----- -Documentation: https://letsencrypt.readthedocs.org/ +Documentation: https://letsencrypt.readthedocs.org Software project: https://github.com/letsencrypt/lets-encrypt-preview From 3382dac793fee6b06618c8aa6d27b8580ab4525e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:17:15 +0000 Subject: [PATCH 055/109] README: FAQ link at the top. --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 5b149abe5..40c054fe3 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,10 @@ Official **documentation**, including `installation instructions`_, is available at https://letsencrypt.readthedocs.org. +Generic information about Let's Encrypt project can be found at +https://letsencrypt.org. Please read `Frequently Asked Questions (FAQ) +`_. + About the Let's Encrypt Client ============================== From a1f5ea8e8ce81e1415c6da98325a39dce1f7f9c9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 09:55:33 +0000 Subject: [PATCH 056/109] Docs: note about pip editable mode. --- docs/contributing.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index f527ba421..eb9854581 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -17,6 +17,15 @@ Now you can install the development packages: ./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing] +.. 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. Any `pip install + .` call will re-install Let's Encrypt in non-editable mode. + + 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% test statement coverage **and** be compliant with the :ref:`coding style `. From 673a6d4f3710718b7f82e48f7de55d6636dfbc04 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:41:04 +0000 Subject: [PATCH 057/109] Docs: move SWIG notes below installation cmd, Mac OS X note. --- docs/using.rst | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index a180a3826..6709ad51a 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -31,13 +31,6 @@ In general: * ``sudo`` is required as a suggested way of running privileged process * `SWIG`_ is required for compiling `M2Crypto`_ - - .. _new-swig: - .. note:: If your operating system uses SWIG 3.0.5+, you will need - to run ``pip install -r requirements-swig-3.0.5.txt -r - requirements.txt`` instead of the standard ``pip - install -r requirements.txt``. - * `Augeas`_ is required for the Python bindings @@ -79,8 +72,6 @@ Fedora sudo ./bootstrap/fedora.sh -.. note:: Fedora 22 uses SWIG 3.0.5+, use the :ref:`modified pip - command for installation `. Centos 7 -------- @@ -106,6 +97,13 @@ Installation virtualenv --no-site-packages -p python2 venv ./venv/bin/pip install -r requirements.txt . +.. note:: If your operating system uses SWIG 3.0.5+, you will need to + run ``pip install -r requirements-swig-3.0.5.txt -r + requirements.txt`` instead. Known affected systems: + + * Fedora 22 + * some versions of Mac OS X + Usage ===== From 1a013eae6e021d763f3a692e10de44add751d96a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:49:18 +0000 Subject: [PATCH 058/109] Docs: no support for setup.py, root, or non-Virtualenv installation. --- docs/using.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 6709ad51a..0014a1192 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -97,6 +97,14 @@ Installation virtualenv --no-site-packages -p python2 venv ./venv/bin/pip install -r requirements.txt . +.. warning:: Please do **not** use ``python setup.py install``. Please + do **not** attempt the installation commands as + superuser/root and/or without Virtualenv_, e.g. ``sudo + python setup.py install``, ``sudo pip install``, ``sudo + ./venv/bin/...``. These modes of operation might corrupt + your operating system and are **not supported** by the + Let's Encrypt team! + .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r requirements.txt`` instead. Known affected systems: @@ -118,3 +126,4 @@ The letsencrypt commandline tool has a builtin help: .. _Augeas: http://augeas.net/ .. _M2Crypto: https://github.com/M2Crypto/M2Crypto .. _SWIG: http://www.swig.org/ +.. _Virtualenv: https://virtualenv.pypa.io From ca6b326371a60e5104e5bd53b34ff502ac0839c3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 11:00:00 +0000 Subject: [PATCH 059/109] Docs: add "Getting the code" section. --- docs/using.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 0014a1192..c829e9c80 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -20,6 +20,25 @@ And follow the instructions. Your new cert will be available in ``/etc/letsencrypt/certs``. +Getting the code +================ + +Please `install Git`_ and run the following commands: + +.. code-block:: shell + + git clone https://github.com/letsencrypt/lets-encrypt-preview + cd lets-encrypt-preview + +Alternatively you could `download the ZIP archive`_ and extract the +snapshot of our repository, but it's strongly recommended to use the +above method instead. + +.. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git +.. _`download the ZIP archive`: + https://github.com/letsencrypt/lets-encrypt-preview/archive/master.zip + + Prerequisites ============= From 8292eab3f7c6803746c2ff857b82a754b275920b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 11:01:56 +0000 Subject: [PATCH 060/109] Docs: add link to Docker docs. --- docs/using.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index c829e9c80..51c5527f6 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -5,9 +5,9 @@ Using the Let's Encrypt client Quick start =========== -Using docker you can quickly get yourself a testing cert. From the +Using Docker_ you can quickly get yourself a testing cert. From the server that the domain your requesting a cert for resolves to, -download docker, and issue the following command +`install Docker`_, issue the following command: .. code-block:: shell @@ -16,9 +16,12 @@ download docker, and issue the following command -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ quay.io/letsencrypt/lets-encrypt-preview:latest -And follow the instructions. Your new cert will be available in +and follow the instructions. Your new cert will be available in ``/etc/letsencrypt/certs``. +.. _Docker: https://docker.com +.. _`install Docker`: https://docs.docker.com/docker/userguide/ + Getting the code ================ From b8ebb0ab161c4a27eaaffa14fe6860b44ed96e29 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 11:03:05 +0000 Subject: [PATCH 061/109] Docs: backticks nit. --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 51c5527f6..29e8902a9 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -138,7 +138,7 @@ Installation Usage ===== -The letsencrypt commandline tool has a builtin help: +The ``letsencrypt`` commandline tool has a builtin help: .. code-block:: shell From 4040fd02045d871601c1866f25d8e2da11d78a48 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 11:21:51 +0000 Subject: [PATCH 062/109] Docs: extend usage section with "letsencrypt auth" call. --- docs/using.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 29e8902a9..20f07d5d1 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -138,6 +138,12 @@ Installation Usage ===== +To get a new certificate run: + +.. code-block:: shell + + ./venv/bin/letsencrypt auth + The ``letsencrypt`` commandline tool has a builtin help: .. code-block:: shell From e176ad8f43e3342a346194875c147b66078c8306 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 15 Jun 2015 18:31:50 +0000 Subject: [PATCH 063/109] Remove old Boulder incompatibility issue --- letsencrypt/network2.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index f2620b4b1..a20194a79 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -257,13 +257,12 @@ class Network(object): # TODO: Boulder does not set Location or Link on update # (c.f. acme-spec #94) - updated_regr = self._regr_from_response( response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, terms_of_service=regr.terms_of_service) if updated_regr != regr: - # TODO: Boulder reregisters with new recoveryToken and new URI raise errors.UnexpectedUpdate(regr) + return updated_regr def agree_to_tos(self, regr): From 4d39699befb583820246580a4ad058828b2e81cb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 10:56:08 +0000 Subject: [PATCH 064/109] Remove doubled :members: from acme errors docs --- docs/pkgs/acme/index.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/pkgs/acme/index.rst b/docs/pkgs/acme/index.rst index 9cca3b795..1c73a4a42 100644 --- a/docs/pkgs/acme/index.rst +++ b/docs/pkgs/acme/index.rst @@ -51,9 +51,6 @@ Errors :members: - :members: - - Utilities --------- From 23c5a1fd90e1140af4455e32f6e2bf2e270923b9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 16:13:53 +0000 Subject: [PATCH 065/109] Docs: "." and functools32 adjustements --- docs/using.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 20f07d5d1..96eb62b05 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -108,7 +108,7 @@ backslash): .. code-block:: shell SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ - ./venv/bin/pip install -r requirements.txt functools32 + ./venv/bin/pip install -r requirements.txt . Installation @@ -129,7 +129,7 @@ Installation .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r - requirements.txt`` instead. Known affected systems: + requirements.txt .`` instead. Known affected systems: * Fedora 22 * some versions of Mac OS X From 50e509604cfc70a776c682a5735572feac7dda6a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 20 Jun 2015 10:20:54 +0000 Subject: [PATCH 066/109] Docs: remove wrong re-install comment --- docs/contributing.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index eb9854581..804cec95c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -20,8 +20,7 @@ Now you can install the development packages: .. 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. Any `pip install - .` call will re-install Let's Encrypt in non-editable mode. + invocations are necessary while developing. This is roughly equivalent to `python setup.py develop`. For more info see `man pip`. From 061282fa6674bb8b8fc50d61167f5ff45fbe8779 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 20:27:40 +0000 Subject: [PATCH 067/109] Store temporary DVSNI files in IConfig.work_dir. --- letsencrypt_apache/dvsni.py | 2 +- letsencrypt_nginx/dvsni.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py index fb78cfced..c25426371 100644 --- a/letsencrypt_apache/dvsni.py +++ b/letsencrypt_apache/dvsni.py @@ -141,7 +141,7 @@ class ApacheDvsni(common.Dvsni): """ ips = " ".join(str(i) for i in ip_addrs) document_root = os.path.join( - self.configurator.config.config_dir, "dvsni_page/") + self.configurator.config.work_dir, "dvsni_page/") # TODO: Python docs is not clear how mutliline string literal # newlines are parsed on different platforms. At least on # Linux (Debian sid), when source file uses CRLF, Python still diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 620d144f6..0697f6e1e 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -124,7 +124,7 @@ class NginxDvsni(common.Dvsni): """ document_root = os.path.join( - self.configurator.config.config_dir, "dvsni_page") + self.configurator.config.work_dir, "dvsni_page") block = [['listen', str(addr)] for addr in addrs] From 960b070c223b5901ce3b6d1ce7a27748f584eaee Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 14:51:39 +0000 Subject: [PATCH 068/109] Dummy use of network2 in revoker --- letsencrypt/revoker.py | 14 +++++++------- letsencrypt/tests/revoker_test.py | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index a1ea27e71..d173a1907 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -16,12 +16,11 @@ import tempfile import Crypto.PublicKey.RSA import M2Crypto -from acme import messages from acme.jose import util as jose_util from letsencrypt import errors from letsencrypt import le_util -from letsencrypt import network +from letsencrypt import network2 from letsencrypt.display import util as display_util from letsencrypt.display import revocation @@ -45,7 +44,9 @@ class Revoker(object): """ def __init__(self, installer, config, no_confirm=False): - self.network = network.Network(config.server) + # XXX + self.network = network2.Network(new_reg_uri=None, key=None, alg=None) + self.installer = installer self.config = config self.no_confirm = no_confirm @@ -238,6 +239,8 @@ class Revoker(object): :returns: TODO """ + # XXX | pylint: disable=unused-variable + # These will both have to change in the future away from M2Crypto # pylint: disable=protected-access certificate = jose_util.ComparableX509(cert._cert) @@ -250,10 +253,7 @@ class Revoker(object): raise errors.LetsEncryptRevokerError( "Corrupted backup key file: %s" % cert.backup_key_path) - # TODO: Catch error associated with already revoked and proceed. - return self.network.send_and_receive_expected( - messages.RevocationRequest.create(certificate=certificate, key=key), - messages.Revocation) + return self.network.revoke(certr=None) # XXX def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use """Remove certificate and key. diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index ae04b5081..35e7d132b 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -63,7 +63,7 @@ class RevokerTest(RevokerBase): def tearDown(self): shutil.rmtree(self.backup_dir) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_key_all(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -89,7 +89,7 @@ class RevokerTest(RevokerBase): self.revoker.revoke_from_key, self.key) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_wrong_key(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -105,7 +105,7 @@ class RevokerTest(RevokerBase): # No revocation went through self.assertEqual(mock_net.call_count, 0) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_cert(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -122,7 +122,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_net.call_count, 1) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_cert_not_found(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -141,7 +141,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_net.call_count, 1) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_menu(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -165,7 +165,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_display.more_info_cert.call_count, 1) @mock.patch("letsencrypt.revoker.logging") - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log): mock_display().confirm_revocation.return_value = True From c5d4f91bf77612be1bfe0a972922f4cb1ab962a5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 13 Jun 2015 13:45:50 +0000 Subject: [PATCH 069/109] Remove old messages and network --- acme/messages.py | 367 ------------------------------- acme/messages_test.py | 480 ----------------------------------------- letsencrypt/network.py | 121 ----------- setup.py | 1 - 4 files changed, 969 deletions(-) delete mode 100644 acme/messages.py delete mode 100644 acme/messages_test.py delete mode 100644 letsencrypt/network.py diff --git a/acme/messages.py b/acme/messages.py deleted file mode 100644 index 6d46f894c..000000000 --- a/acme/messages.py +++ /dev/null @@ -1,367 +0,0 @@ -"""ACME protocol v00 messages. - -.. warning:: This module is an implementation of the draft `ACME - protocol version 00`_, and not the "RESTified" `ACME protocol version - 01`_ or later. It should work with `older Node.js implementation`_, - but will definitely not work with Boulder_. It is kept for reference - purposes only. - - -.. _`ACME protocol version 00`: - https://github.com/letsencrypt/acme-spec/blob/v00/draft-barnes-acme.md - -.. _`ACME protocol version 01`: - https://github.com/letsencrypt/acme-spec/blob/v01/draft-barnes-acme.md - -.. _Boulder: https://github.com/letsencrypt/boulder - -.. _`older Node.js implementation`: - https://github.com/letsencrypt/node-acme/commit/f42aa5b7fad4cd2fc289653c4ab14f18052367b3 - - -""" -import jsonschema - -from acme import challenges -from acme import errors -from acme import jose -from acme import other -from acme import util - - -class Message(jose.TypedJSONObjectWithFields): - # _fields_to_partial_json | pylint: disable=abstract-method - # pylint: disable=too-few-public-methods - """ACME message.""" - TYPES = {} - type_field_name = "type" - - schema = NotImplemented - """JSON schema the object is tested against in :meth:`from_json`. - - Subclasses must overrride it with a value that is acceptable by - :func:`jsonschema.validate`, most probably using - :func:`acme.util.load_schema`. - - """ - - @classmethod - def from_json(cls, jobj): - """Deserialize from (possibly invalid) JSON object. - - Note that the input ``jobj`` has not been sanitized in any way. - - :param jobj: JSON object. - - :raises acme.errors.SchemaValidationError: if the input - JSON object could not be validated against JSON schema specified - in :attr:`schema`. - :raises acme.jose.errors.DeserializationError: for any - other generic error in decoding. - - :returns: instance of the class - - """ - msg_cls = cls.get_type_cls(jobj) - - # TODO: is that schema testing still relevant? - try: - jsonschema.validate(jobj, msg_cls.schema) - except jsonschema.ValidationError as error: - raise errors.SchemaValidationError(error) - - return super(Message, cls).from_json(jobj) - - -@Message.register # pylint: disable=too-few-public-methods -class Challenge(Message): - """ACME "challenge" message. - - :ivar str nonce: Random data, **not** base64-encoded. - :ivar list challenges: List of - :class:`~acme.challenges.Challenge` objects. - - .. todo:: - 1. can challenges contain two challenges of the same type? - 2. can challenges contain duplicates? - 3. check "combinations" indices are in valid range - 4. turn "combinations" elements into sets? - 5. turn "combinations" into set? - - """ - typ = "challenge" - schema = util.load_schema(typ) - - session_id = jose.Field("sessionID") - nonce = jose.Field("nonce", encoder=jose.b64encode, - decoder=jose.decode_b64jose) - challenges = jose.Field("challenges") - combinations = jose.Field("combinations", omitempty=True, default=()) - - @challenges.decoder - def challenges(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(challenges.Challenge.from_json(chall) for chall in value) - - @property - def resolved_combinations(self): - """Combinations with challenges instead of indices.""" - return tuple(tuple(self.challenges[idx] for idx in combo) - for combo in self.combinations) - - -@Message.register # pylint: disable=too-few-public-methods -class ChallengeRequest(Message): - """ACME "challengeRequest" message.""" - typ = "challengeRequest" - schema = util.load_schema(typ) - identifier = jose.Field("identifier") - - -@Message.register # pylint: disable=too-few-public-methods -class Authorization(Message): - """ACME "authorization" message. - - :ivar jwk: :class:`acme.jose.JWK` - - """ - typ = "authorization" - schema = util.load_schema(typ) - - recovery_token = jose.Field("recoveryToken", omitempty=True) - identifier = jose.Field("identifier", omitempty=True) - jwk = jose.Field("jwk", decoder=jose.JWK.from_json, omitempty=True) - - -@Message.register -class AuthorizationRequest(Message): - """ACME "authorizationRequest" message. - - :ivar str nonce: Random data from the corresponding - :attr:`Challenge.nonce`, **not** base64-encoded. - :ivar list responses: List of completed challenges ( - :class:`acme.challenges.ChallengeResponse`). - :ivar signature: Signature (:class:`acme.other.Signature`). - - """ - typ = "authorizationRequest" - schema = util.load_schema(typ) - - session_id = jose.Field("sessionID") - nonce = jose.Field("nonce", encoder=jose.b64encode, - decoder=jose.decode_b64jose) - responses = jose.Field("responses") - signature = jose.Field("signature", decoder=other.Signature.from_json) - contact = jose.Field("contact", omitempty=True, default=()) - - @responses.decoder - def responses(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(challenges.ChallengeResponse.from_json(chall) - for chall in value) - - @classmethod - def create(cls, name, key, sig_nonce=None, **kwargs): - """Create signed "authorizationRequest". - - :param str name: Hostname - - :param key: Key used for signing. - :type key: :class:`Crypto.PublicKey.RSA` - - :param str sig_nonce: Nonce used for signature. Useful for testing. - :kwargs: Any other arguments accepted by the class constructor. - - :returns: Signed "authorizationRequest" ACME message. - :rtype: :class:`AuthorizationRequest` - - """ - # pylint: disable=too-many-arguments - signature = other.Signature.from_msg( - name + kwargs["nonce"], key, sig_nonce) - return cls( - signature=signature, contact=kwargs.pop("contact", ()), **kwargs) - - def verify(self, name): - """Verify signature. - - .. warning:: Caller must check that the public key encoded in the - :attr:`signature`'s :class:`acme.jose.JWK` object - is the correct key for a given context. - - :param str name: Hostname - - :returns: True iff ``signature`` can be verified, False otherwise. - :rtype: bool - - """ - # self.signature is not Field | pylint: disable=no-member - return self.signature.verify(name + self.nonce) - - -@Message.register # pylint: disable=too-few-public-methods -class Certificate(Message): - """ACME "certificate" message. - - :ivar certificate: The certificate (:class:`M2Crypto.X509.X509` - wrapped in :class:`acme.util.ComparableX509`). - - :ivar list chain: Chain of certificates (:class:`M2Crypto.X509.X509` - wrapped in :class:`acme.util.ComparableX509` ). - - """ - typ = "certificate" - schema = util.load_schema(typ) - - certificate = jose.Field("certificate", encoder=jose.encode_cert, - decoder=jose.decode_cert) - chain = jose.Field("chain", omitempty=True, default=()) - refresh = jose.Field("refresh", omitempty=True) - - @chain.decoder - def chain(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(jose.decode_cert(cert) for cert in value) - - @chain.encoder - def chain(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(jose.encode_cert(cert) for cert in value) - - -@Message.register -class CertificateRequest(Message): - """ACME "certificateRequest" message. - - :ivar csr: Certificate Signing Request (:class:`M2Crypto.X509.Request` - wrapped in :class:`acme.util.ComparableX509`. - :ivar signature: Signature (:class:`acme.other.Signature`). - - """ - typ = "certificateRequest" - schema = util.load_schema(typ) - - csr = jose.Field("csr", encoder=jose.encode_csr, - decoder=jose.decode_csr) - signature = jose.Field("signature", decoder=other.Signature.from_json) - - @classmethod - def create(cls, key, sig_nonce=None, **kwargs): - """Create signed "certificateRequest". - - :param key: Key used for signing. - :type key: :class:`Crypto.PublicKey.RSA` - - :param str sig_nonce: Nonce used for signature. Useful for testing. - :kwargs: Any other arguments accepted by the class constructor. - - :returns: Signed "certificateRequest" ACME message. - :rtype: :class:`CertificateRequest` - - """ - return cls(signature=other.Signature.from_msg( - kwargs["csr"].as_der(), key, sig_nonce), **kwargs) - - def verify(self): - """Verify signature. - - .. warning:: Caller must check that the public key encoded in the - :attr:`signature`'s :class:`acme.jose.JWK` object - is the correct key for a given context. - - :returns: True iff ``signature`` can be verified, False otherwise. - :rtype: bool - - """ - # self.signature is not Field | pylint: disable=no-member - return self.signature.verify(self.csr.as_der()) - - -@Message.register # pylint: disable=too-few-public-methods -class Defer(Message): - """ACME "defer" message.""" - typ = "defer" - schema = util.load_schema(typ) - - token = jose.Field("token") - interval = jose.Field("interval", omitempty=True) - message = jose.Field("message", omitempty=True) - - -@Message.register # pylint: disable=too-few-public-methods -class Error(Message): - """ACME "error" message.""" - typ = "error" - schema = util.load_schema(typ) - - error = jose.Field("error") - message = jose.Field("message", omitempty=True) - more_info = jose.Field("moreInfo", omitempty=True) - - MESSAGE_CODES = { - "malformed": "The request message was malformed", - "unauthorized": "The client lacks sufficient authorization", - "serverInternal": "The server experienced an internal error", - "notSupported": "The request type is not supported", - "unknown": "The server does not recognize an ID/token in the request", - "badCSR": "The CSR is unacceptable (e.g., due to a short key)", - } - - -@Message.register # pylint: disable=too-few-public-methods -class Revocation(Message): - """ACME "revocation" message.""" - typ = "revocation" - schema = util.load_schema(typ) - - -@Message.register -class RevocationRequest(Message): - """ACME "revocationRequest" message. - - :ivar certificate: Certificate (:class:`M2Crypto.X509.X509` - wrapped in :class:`acme.util.ComparableX509`). - :ivar signature: Signature (:class:`acme.other.Signature`). - - """ - typ = "revocationRequest" - schema = util.load_schema(typ) - - certificate = jose.Field("certificate", decoder=jose.decode_cert, - encoder=jose.encode_cert) - signature = jose.Field("signature", decoder=other.Signature.from_json) - - @classmethod - def create(cls, key, sig_nonce=None, **kwargs): - """Create signed "revocationRequest". - - :param key: Key used for signing. - :type key: :class:`Crypto.PublicKey.RSA` - - :param str sig_nonce: Nonce used for signature. Useful for testing. - :kwargs: Any other arguments accepted by the class constructor. - - :returns: Signed "revocationRequest" ACME message. - :rtype: :class:`RevocationRequest` - - """ - return cls(signature=other.Signature.from_msg( - kwargs["certificate"].as_der(), key, sig_nonce), **kwargs) - - def verify(self): - """Verify signature. - - .. warning:: Caller must check that the public key encoded in the - :attr:`signature`'s :class:`acme.jose.JWK` object - is the correct key for a given context. - - :returns: True iff ``signature`` can be verified, False otherwise. - :rtype: bool - - """ - # self.signature is not Field | pylint: disable=no-member - return self.signature.verify(self.certificate.as_der()) - - -@Message.register # pylint: disable=too-few-public-methods -class StatusRequest(Message): - """ACME "statusRequest" message.""" - typ = "statusRequest" - schema = util.load_schema(typ) - token = jose.Field("token") diff --git a/acme/messages_test.py b/acme/messages_test.py deleted file mode 100644 index baff2a21a..000000000 --- a/acme/messages_test.py +++ /dev/null @@ -1,480 +0,0 @@ -"""Tests for acme.messages.""" -import os -import pkg_resources -import unittest - -import Crypto.PublicKey.RSA -import M2Crypto - -from acme import challenges -from acme import errors -from acme import jose -from acme import other - - -KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( - pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) -CERT = jose.ComparableX509(M2Crypto.X509.load_cert( - pkg_resources.resource_filename( - 'letsencrypt.tests', os.path.join('testdata', 'cert.pem')))) -CSR = jose.ComparableX509(M2Crypto.X509.load_request( - pkg_resources.resource_filename( - 'letsencrypt.tests', os.path.join('testdata', 'csr.pem')))) -CSR2 = jose.ComparableX509(M2Crypto.X509.load_request( - pkg_resources.resource_filename( - 'acme.jose', os.path.join('testdata', 'csr2.pem')))) - - -class MessageTest(unittest.TestCase): - """Tests for acme.messages.Message.""" - - def setUp(self): - # pylint: disable=missing-docstring,too-few-public-methods - from acme.messages import Message - - class MockParentMessage(Message): - # pylint: disable=abstract-method - TYPES = {} - - @MockParentMessage.register - class MockMessage(MockParentMessage): - typ = 'test' - schema = { - 'type': 'object', - 'properties': { - 'price': {'type': 'number'}, - 'name': {'type': 'string'}, - }, - } - price = jose.Field('price') - name = jose.Field('name') - - self.parent_cls = MockParentMessage - self.msg = MockMessage(price=123, name='foo') - - def test_from_json_validates(self): - self.assertRaises(errors.SchemaValidationError, - self.parent_cls.from_json, - {'type': 'test', 'price': 'asd'}) - - -class ChallengeTest(unittest.TestCase): - - def setUp(self): - challs = ( - challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'), - challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'), - challenges.RecoveryToken(), - ) - combinations = ((0, 2), (1, 2)) - - from acme.messages import Challenge - self.msg = Challenge( - session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4', - nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9', - challenges=challs, combinations=combinations) - - self.jmsg_to = { - 'type': 'challenge', - 'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4', - 'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ', - 'challenges': challs, - 'combinations': combinations, - } - - self.jmsg_from = { - 'type': 'challenge', - 'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4', - 'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ', - 'challenges': [chall.to_json() for chall in challs], - 'combinations': [[0, 2], [1, 2]], # TODO array tuples - } - - def test_resolved_combinations(self): - self.assertEqual(self.msg.resolved_combinations, ( - ( - challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'), - challenges.RecoveryToken() - ), - ( - challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'), - challenges.RecoveryToken(), - ) - )) - - def test_to_partial_json(self): - self.assertEqual(self.msg.to_partial_json(), self.jmsg_to) - - def test_from_json(self): - from acme.messages import Challenge - self.assertEqual(Challenge.from_json(self.jmsg_from), self.msg) - - def test_json_without_optionals(self): - del self.jmsg_from['combinations'] - del self.jmsg_to['combinations'] - - from acme.messages import Challenge - msg = Challenge.from_json(self.jmsg_from) - - self.assertEqual(msg.combinations, ()) - self.assertEqual(msg.to_partial_json(), self.jmsg_to) - - -class ChallengeRequestTest(unittest.TestCase): - - def setUp(self): - from acme.messages import ChallengeRequest - self.msg = ChallengeRequest(identifier='example.com') - - self.jmsg = { - 'type': 'challengeRequest', - 'identifier': 'example.com', - } - - def test_to_partial_json(self): - self.assertEqual(self.msg.to_partial_json(), self.jmsg) - - def test_from_json(self): - from acme.messages import ChallengeRequest - self.assertEqual(ChallengeRequest.from_json(self.jmsg), self.msg) - - -class AuthorizationTest(unittest.TestCase): - - def setUp(self): - jwk = jose.JWKRSA(key=KEY.publickey()) - - from acme.messages import Authorization - self.msg = Authorization(recovery_token='tok', jwk=jwk, - identifier='example.com') - - self.jmsg = { - 'type': 'authorization', - 'recoveryToken': 'tok', - 'identifier': 'example.com', - 'jwk': jwk, - } - - def test_to_partial_json(self): - self.assertEqual(self.msg.to_partial_json(), self.jmsg) - - def test_from_json(self): - self.jmsg['jwk'] = self.jmsg['jwk'].to_partial_json() - - from acme.messages import Authorization - self.assertEqual(Authorization.from_json(self.jmsg), self.msg) - - def test_json_without_optionals(self): - del self.jmsg['recoveryToken'] - del self.jmsg['identifier'] - del self.jmsg['jwk'] - - from acme.messages import Authorization - msg = Authorization.from_json(self.jmsg) - - self.assertTrue(msg.recovery_token is None) - self.assertTrue(msg.identifier is None) - self.assertTrue(msg.jwk is None) - self.assertEqual(self.jmsg, msg.to_partial_json()) - - -class AuthorizationRequestTest(unittest.TestCase): - - def setUp(self): - self.responses = ( - challenges.SimpleHTTPResponse(path='Hf5GrX4Q7EBax9hc2jJnfw'), - None, # null - challenges.RecoveryTokenResponse(token='23029d88d9e123e'), - ) - self.contact = ("mailto:cert-admin@example.com", "tel:+12025551212") - signature = other.Signature( - alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.publickey()), - sig='-v\xd8\xc2\xa3\xba0\xd6\x92\x16\xb5.\xbe\xa1[\x04\xbe' - '\x1b\xa1X\xd2)\x18\x94\x8f\xd7\xd0\xc0\xbbcI`W\xdf v' - '\xe4\xed\xe8\x03J\xe8\xc8 Date: Thu, 11 Jun 2015 16:05:49 +0000 Subject: [PATCH 070/109] Remove old messages schemata. --- acme/schemata/authorization.json | 21 ---- acme/schemata/authorizationRequest.json | 38 ------- acme/schemata/certificate.json | 25 ----- acme/schemata/certificateRequest.json | 19 ---- acme/schemata/challenge.json | 36 ------- acme/schemata/challengeRequest.json | 15 --- acme/schemata/challengeobject.json | 130 ------------------------ acme/schemata/defer.json | 21 ---- acme/schemata/error.json | 21 ---- acme/schemata/jwk.json | 19 ---- acme/schemata/responseobject.json | 75 -------------- acme/schemata/revocation.json | 12 --- acme/schemata/revocationRequest.json | 18 ---- acme/schemata/signature.json | 71 ------------- acme/schemata/statusRequest.json | 15 --- 15 files changed, 536 deletions(-) delete mode 100644 acme/schemata/authorization.json delete mode 100644 acme/schemata/authorizationRequest.json delete mode 100644 acme/schemata/certificate.json delete mode 100644 acme/schemata/certificateRequest.json delete mode 100644 acme/schemata/challenge.json delete mode 100644 acme/schemata/challengeRequest.json delete mode 100644 acme/schemata/challengeobject.json delete mode 100644 acme/schemata/defer.json delete mode 100644 acme/schemata/error.json delete mode 100644 acme/schemata/jwk.json delete mode 100644 acme/schemata/responseobject.json delete mode 100644 acme/schemata/revocation.json delete mode 100644 acme/schemata/revocationRequest.json delete mode 100644 acme/schemata/signature.json delete mode 100644 acme/schemata/statusRequest.json diff --git a/acme/schemata/authorization.json b/acme/schemata/authorization.json deleted file mode 100644 index 122f263e1..000000000 --- a/acme/schemata/authorization.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/authorization#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for an authorization message", - "type": "object", - "required": ["type"], - "properties": { - "type" : { - "enum" : [ "authorization" ] - }, - "recoveryToken" : { - "type": "string" - }, - "identifier" : { - "type": "string" - }, - "jwk": { - "$ref": "file:acme/schemata/jwk.json" - } - } -} diff --git a/acme/schemata/authorizationRequest.json b/acme/schemata/authorizationRequest.json deleted file mode 100644 index 2d4371cb8..000000000 --- a/acme/schemata/authorizationRequest.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/authorizationRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for an authorizationRequest message", - "type": "object", - "required": ["type", "sessionID", "nonce", "signature", "responses"], - "properties": { - "type" : { - "enum" : [ "authorizationRequest" ] - }, - "sessionID" : { - "type" : "string" - }, - "nonce" : { - "type": "string" - }, - "signature" : { - "$ref": "file:acme/schemata/signature.json" - }, - "responses": { - "type": "array", - "minItems": 1, - "items": { - "anyOf": [ - { "$ref": "file:acme/schemata/responseobject.json" }, - { "type": "null" } - ] - } - }, - "contact": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - } -} diff --git a/acme/schemata/certificate.json b/acme/schemata/certificate.json deleted file mode 100644 index 1d4e98947..000000000 --- a/acme/schemata/certificate.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/certificate#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a certificate message", - "type": "object", - "required": ["type", "certificate"], - "properties": { - "type" : { - "enum" : [ "certificate" ] - }, - "certificate" : { - "type" : "string" - }, - "chain" : { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - }, - "refresh" : { - "type": "string" - } - } -} diff --git a/acme/schemata/certificateRequest.json b/acme/schemata/certificateRequest.json deleted file mode 100644 index ef3e18f98..000000000 --- a/acme/schemata/certificateRequest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/certificateRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a certificateRequest message", - "type": "object", - "required": ["type", "csr", "signature"], - "properties": { - "type" : { - "enum" : [ "certificateRequest" ] - }, - "csr" : { - "type" : "string" , - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "signature" : { - "$ref": "file:acme/schemata/signature.json" - } - } -} diff --git a/acme/schemata/challenge.json b/acme/schemata/challenge.json deleted file mode 100644 index 978fcd4c4..000000000 --- a/acme/schemata/challenge.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/challenge#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a challenge message", - "type": "object", - "required": ["type", "sessionID", "nonce", "challenges"], - "properties": { - "type" : { - "enum" : [ "challenge" ] - }, - "sessionID" : { - "type" : "string" - }, - "nonce" : { - "type": "string" - }, - "challenges": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "file:acme/schemata/challengeobject.json" - } - }, - "combinations": { - "type": "array", - "minItems": 1, - "items": { - "type": "array", - "minItems": 1, - "items": { - "type": "integer" - } - } - } - } -} diff --git a/acme/schemata/challengeRequest.json b/acme/schemata/challengeRequest.json deleted file mode 100644 index 0762fa9c8..000000000 --- a/acme/schemata/challengeRequest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/challengeRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a challengeRequest message", - "type": "object", - "required": ["type", "identifier"], - "properties": { - "type" : { - "enum" : [ "challengeRequest" ] - }, - "identifier" : { - "type": "string" - } - } -} diff --git a/acme/schemata/challengeobject.json b/acme/schemata/challengeobject.json deleted file mode 100644 index 7709f315d..000000000 --- a/acme/schemata/challengeobject.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/challengeobject#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Subschema for an individual challenge (within challenge)", - "anyOf": [ - { "type": "object", - "required": ["type", "token"], - "properties": { - "type": { - "enum" : [ "simpleHttp" ] - }, - "token": { - "type": "string" - } - } - }, - { "type": "object", - "required": ["type", "r", "nonce"], - "properties": { - "type": { - "enum" : [ "dvsni" ] - }, - "r": { - "type" : [ "string" ], - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "nonce": { - "type": "string", - "pattern": "^[0-9a-f]+$" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "recoveryContact" ] - }, - "activationURL": { - "type" : "string" - }, - "successURL": { - "type": "string" - }, - "contact": { - "type": "string" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "recoveryToken" ] - } - } - }, - { "type": "object", - "required": ["type", "alg", "nonce", "hints"], - "properties": { - "type": { - "enum" : [ "proofOfPossession" ] - }, - "alg": { - "type": "string" - }, - "nonce": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "hints": { - "type": "object", - "properties": { - "jwk": { - "type": "object" - }, - "certFingerprints": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[0-9a-f]+$" - } - }, - "subjectKeyIdentifiers": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[0-9a-f]+$" - } - }, - "serialNumbers": { - "type": "array", - "minItems": 1, - "items": { - "type": "integer" - } - }, - "issuers": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - }, - "authorizedFor": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - } - } - } - }, - { "type": "object", - "required": ["type", "token"], - "properties": { - "type": { - "enum" : [ "dns" ] - }, - "token": { - "type": "string" - } - } - } - ] -} diff --git a/acme/schemata/defer.json b/acme/schemata/defer.json deleted file mode 100644 index 21edd614b..000000000 --- a/acme/schemata/defer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/defer#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a defer message", - "type": "object", - "required": ["type", "token"], - "properties": { - "type" : { - "enum" : [ "defer" ] - }, - "token" : { - "type": "string" - }, - "interval" : { - "type": "integer" - }, - "message": { - "type": "string" - } - } -} diff --git a/acme/schemata/error.json b/acme/schemata/error.json deleted file mode 100644 index 359506b52..000000000 --- a/acme/schemata/error.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/error#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for an error message", - "type": "object", - "required": ["type", "error"], - "properties": { - "type" : { - "enum" : [ "error" ] - }, - "error" : { - "enum" : [ "malformed", "unauthorized", "serverInternal", "nonSupported", "unknown", "badCSR" ] - }, - "message" : { - "type": "string" - }, - "moreInfo": { - "type": "string" - } - } -} diff --git a/acme/schemata/jwk.json b/acme/schemata/jwk.json deleted file mode 100644 index b9cca8840..000000000 --- a/acme/schemata/jwk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/jwk#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a jwk (**kty RSA/e=65537 ONLY**)", - "type": "object", - "required": ["kty", "e", "n"], - "properties": { - "kty": { - "enum" : [ "RSA" ] - }, - "e": { - "enum" : [ "AQAB" ] - }, - "n": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - } - } -} diff --git a/acme/schemata/responseobject.json b/acme/schemata/responseobject.json deleted file mode 100644 index 5773f3a73..000000000 --- a/acme/schemata/responseobject.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/responseobject#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Subschema for an individual challenge response (within authorizationRequest)", - "anyOf": [ - { "type": "object", - "required": ["type", "path"], - "properties": { - "type": { - "enum" : [ "simpleHttp" ] - }, - "path": { - "type": "string" - } - } - }, - { "type": "object", - "required": ["type", "s"], - "properties": { - "type": { - "enum" : [ "dvsni" ] - }, - "s": { - "type" : [ "string" ], - "pattern": "^[-_=0-9A-Za-z]+$" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "recoveryContact" ] - }, - "token": { - "type" : "string" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "recoveryToken" ] - }, - "token": { - "type" : "string" - } - } - }, - { "type": "object", - "required": ["type", "nonce", "signature"], - "properties": { - "type": { - "enum" : [ "proofOfPossession" ] - }, - "nonce": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "signature": { - "$ref": "file:acme/schemata/signature.json" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "dns" ] - } - } - } - ] -} diff --git a/acme/schemata/revocation.json b/acme/schemata/revocation.json deleted file mode 100644 index 53455d506..000000000 --- a/acme/schemata/revocation.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/revocation#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a revocation message", - "type": "object", - "required": ["type"], - "properties": { - "type" : { - "enum" : [ "revocation" ] - } - } -} diff --git a/acme/schemata/revocationRequest.json b/acme/schemata/revocationRequest.json deleted file mode 100644 index 7559d0ee0..000000000 --- a/acme/schemata/revocationRequest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/revocationRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a revocationRequest message", - "type": "object", - "required": ["type", "certificate", "signature"], - "properties": { - "type" : { - "enum" : [ "revocationRequest" ] - }, - "certificate" : { - "type" : "string" - }, - "signature" : { - "$ref": "file:acme/schemata/signature.json" - } - } -} diff --git a/acme/schemata/signature.json b/acme/schemata/signature.json deleted file mode 100644 index e70652e7c..000000000 --- a/acme/schemata/signature.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/signature#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a signature (alg RS256/e=65537 or P-256 ONLY)", - "type": "object", - "required": ["alg", "nonce", "sig", "jwk"], - "properties": { - "anyOf": [ - { - "alg" : { - "enum" : [ "RS256" ] - }, - "nonce" : { - "type" : "string" - }, - "sig" : { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "jwk": { - "type": "object", - "required": ["kty", "e", "n"], - "properties": { - "kty": { - "enum" : [ "RSA" ] - }, - "e": { - "enum" : [ "AQAB" ] - }, - "n": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - } - } - } - }, - { - "alg" : { - "enum" : [ "ES256" ] - }, - "nonce" : { - "type" : "string" - }, - "sig" : { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "jwk": { - "type": "object", - "required": ["kty", "crv", "x", "y"], - "properties": { - "kty": { - "enum" : [ "EC" ] - }, - "crv": { - "enum" : [ "P-256" ] - }, - "x": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "y": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - } - } - } - } - ] - } -} diff --git a/acme/schemata/statusRequest.json b/acme/schemata/statusRequest.json deleted file mode 100644 index 8e4221cbe..000000000 --- a/acme/schemata/statusRequest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/statusRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a statusRequest message", - "type": "object", - "required": ["type", "token"], - "properties": { - "type" : { - "enum" : [ "statusRequest" ] - }, - "token" : { - "type": "string" - } - } -} From aa6faadb5c1d99c5d2026cac9a62193ead8ebd01 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 15:07:17 +0000 Subject: [PATCH 071/109] Add ChallangeResponseTest --- acme/challenges_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/acme/challenges_test.py b/acme/challenges_test.py index 4c61c0e3d..f0b025ad3 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -18,6 +18,13 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) +class ChallengeResponseTest(unittest.TestCase): + + def test_from_json_none(self): + from acme.challenges import ChallengeResponse + self.assertTrue(ChallengeResponse.from_json(None) is None) + + class SimpleHTTPTest(unittest.TestCase): def setUp(self): From 71a01d139ca25eae0548b35cfc5520fa5c3a808b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 14:55:11 +0000 Subject: [PATCH 072/109] Rename network2 to network. --- docs/api/network2.rst | 5 ---- examples/restified.py | 4 +-- letsencrypt/auth_handler.py | 2 +- letsencrypt/client.py | 6 ++-- letsencrypt/{network2.py => network.py} | 0 letsencrypt/revoker.py | 4 +-- letsencrypt/tests/auth_handler_test.py | 4 +-- letsencrypt/tests/client_test.py | 6 ++-- .../{network2_test.py => network_test.py} | 28 +++++++++---------- letsencrypt/tests/revoker_test.py | 12 ++++---- 10 files changed, 33 insertions(+), 38 deletions(-) delete mode 100644 docs/api/network2.rst rename letsencrypt/{network2.py => network.py} (100%) rename letsencrypt/tests/{network2_test.py => network_test.py} (97%) diff --git a/docs/api/network2.rst b/docs/api/network2.rst deleted file mode 100644 index a73308e1b..000000000 --- a/docs/api/network2.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.network2` ---------------------------- - -.. automodule:: letsencrypt.network2 - :members: diff --git a/examples/restified.py b/examples/restified.py index c0252c1eb..07c773575 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -7,7 +7,7 @@ import M2Crypto from acme import messages2 from acme import jose -from letsencrypt import network2 +from letsencrypt import network logger = logging.getLogger() @@ -17,7 +17,7 @@ NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg' key = jose.JWKRSA.load(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) -net = network2.Network(NEW_REG_URL, key) +net = network.Network(NEW_REG_URL, key) regr = net.register(contact=( 'mailto:cert-admin@example.com', 'tel:+12025551212')) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 5665fe83d..d801613c5 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -24,7 +24,7 @@ class AuthHandler(object): :ivar network: Network object for sending and receiving authorization messages - :type network: :class:`letsencrypt.network2.Network` + :type network: :class:`letsencrypt.network.Network` :ivar account: Client's Account :type account: :class:`letsencrypt.account.Account` diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 17bee6069..d059a777e 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -16,7 +16,7 @@ from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util -from letsencrypt import network2 +from letsencrypt import network from letsencrypt import reverter from letsencrypt import revoker from letsencrypt import storage @@ -29,7 +29,7 @@ class Client(object): """ACME protocol client. :ivar network: Network object for sending and receiving messages - :type network: :class:`letsencrypt.network2.Network` + :type network: :class:`letsencrypt.network.Network` :ivar account: Account object used for registration :type account: :class:`letsencrypt.account.Account` @@ -62,7 +62,7 @@ class Client(object): self.installer = installer # TODO: Allow for other alg types besides RS256 - self.network = network2.Network( + self.network = network.Network( config.server, jwk.JWKRSA.load(self.account.key.pem), verify_ssl=(not config.no_verify_ssl)) diff --git a/letsencrypt/network2.py b/letsencrypt/network.py similarity index 100% rename from letsencrypt/network2.py rename to letsencrypt/network.py diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index d173a1907..0d3bd8e79 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -20,7 +20,7 @@ from acme.jose import util as jose_util from letsencrypt import errors from letsencrypt import le_util -from letsencrypt import network2 +from letsencrypt import network from letsencrypt.display import util as display_util from letsencrypt.display import revocation @@ -45,7 +45,7 @@ class Revoker(object): """ def __init__(self, installer, config, no_confirm=False): # XXX - self.network = network2.Network(new_reg_uri=None, key=None, alg=None) + self.network = network.Network(new_reg_uri=None, key=None, alg=None) self.installer = installer self.config = config diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 8cbc0e604..fddf508b2 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -10,7 +10,7 @@ from acme import messages2 from letsencrypt import errors from letsencrypt import le_util -from letsencrypt import network2 +from letsencrypt import network from letsencrypt.tests import acme_util @@ -86,7 +86,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_dv_auth.perform.side_effect = gen_auth_resp self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) - self.mock_net = mock.MagicMock(spec=network2.Network) + self.mock_net = mock.MagicMock(spec=network.Network) self.handler = AuthHandler( self.mock_dv_auth, self.mock_cont_auth, diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1fb9c2a03..79e2597ea 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -26,14 +26,14 @@ class ClientTest(unittest.TestCase): self.account = mock.MagicMock(**{"key.pem": KEY}) from letsencrypt.client import Client - with mock.patch("letsencrypt.client.network2") as network2: + with mock.patch("letsencrypt.client.network") as network: self.client = Client( config=self.config, account_=self.account, dv_auth=None, installer=None) - self.network2 = network2 + self.network = network def test_init_network_verify_ssl(self): - self.network2.Network.assert_called_once_with( + self.network.Network.assert_called_once_with( mock.ANY, mock.ANY, verify_ssl=True) @mock.patch("letsencrypt.client.zope.component.getUtility") diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network_test.py similarity index 97% rename from letsencrypt/tests/network2_test.py rename to letsencrypt/tests/network_test.py index 3f745ffa7..c0522c2fb 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network_test.py @@ -1,4 +1,4 @@ -"""Tests for letsencrypt.network2.""" +"""Tests for letsencrypt.network.""" import datetime import httplib import os @@ -36,7 +36,7 @@ KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( class NetworkTest(unittest.TestCase): - """Tests for letsencrypt.network2.Network.""" + """Tests for letsencrypt.network.Network.""" # pylint: disable=too-many-instance-attributes,too-many-public-methods @@ -44,7 +44,7 @@ class NetworkTest(unittest.TestCase): self.verify_ssl = mock.MagicMock() self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) - from letsencrypt.network2 import Network + from letsencrypt.network import Network self.net = Network( new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) @@ -167,14 +167,14 @@ class NetworkTest(unittest.TestCase): # pylint: disable=protected-access self.net._check_response(self.response) - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_get_requests_error_passthrough(self, requests_mock): requests_mock.exceptions = requests.exceptions requests_mock.get.side_effect = requests.exceptions.RequestException # pylint: disable=protected-access self.assertRaises(errors.NetworkError, self.net._get, 'uri') - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_get(self, requests_mock): # pylint: disable=protected-access self.net._check_response = mock.MagicMock() @@ -186,7 +186,7 @@ class NetworkTest(unittest.TestCase): # pylint: disable=protected-access self.net._wrap_in_jws = self.wrap_in_jws - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_post_requests_error_passthrough(self, requests_mock): requests_mock.exceptions = requests.exceptions requests_mock.post.side_effect = requests.exceptions.RequestException @@ -195,7 +195,7 @@ class NetworkTest(unittest.TestCase): self.assertRaises( errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_post(self, requests_mock): # pylint: disable=protected-access self.net._check_response = mock.MagicMock() @@ -206,7 +206,7 @@ class NetworkTest(unittest.TestCase): self.net._check_response.assert_called_once_with( requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_post_replay_nonce_handling(self, requests_mock): # pylint: disable=protected-access self.net._check_response = mock.MagicMock() @@ -233,7 +233,7 @@ class NetworkTest(unittest.TestCase): self.assertRaises( errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - @mock.patch('letsencrypt.client.network2.requests') + @mock.patch('letsencrypt.client.network.requests') def test_get_post_verify_ssl(self, requests_mock): # pylint: disable=protected-access self._mock_wrap_in_jws() @@ -372,7 +372,7 @@ class NetworkTest(unittest.TestCase): datetime.datetime(1999, 12, 31, 23, 59, 59), self.net.retry_after(response=self.response, default=10)) - @mock.patch('letsencrypt.network2.datetime') + @mock.patch('letsencrypt.network.datetime') def test_retry_after_invalid(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta @@ -382,7 +382,7 @@ class NetworkTest(unittest.TestCase): datetime.datetime(2015, 3, 27, 0, 0, 10), self.net.retry_after(response=self.response, default=10)) - @mock.patch('letsencrypt.network2.datetime') + @mock.patch('letsencrypt.network.datetime') def test_retry_after_seconds(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta @@ -392,7 +392,7 @@ class NetworkTest(unittest.TestCase): datetime.datetime(2015, 3, 27, 0, 0, 50), self.net.retry_after(response=self.response, default=10)) - @mock.patch('letsencrypt.network2.datetime') + @mock.patch('letsencrypt.network.datetime') def test_retry_after_missing(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta @@ -435,8 +435,8 @@ class NetworkTest(unittest.TestCase): errors.NetworkError, self.net.request_issuance, CSR, (self.authzr,)) - @mock.patch('letsencrypt.network2.datetime') - @mock.patch('letsencrypt.network2.time') + @mock.patch('letsencrypt.network.datetime') + @mock.patch('letsencrypt.network.time') def test_poll_and_request_issuance(self, time_mock, dt_mock): # clock.dt | pylint: disable=no-member clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index 35e7d132b..cd86594fd 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -63,7 +63,7 @@ class RevokerTest(RevokerBase): def tearDown(self): shutil.rmtree(self.backup_dir) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_key_all(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -89,7 +89,7 @@ class RevokerTest(RevokerBase): self.revoker.revoke_from_key, self.key) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_wrong_key(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -105,7 +105,7 @@ class RevokerTest(RevokerBase): # No revocation went through self.assertEqual(mock_net.call_count, 0) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_cert(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -122,7 +122,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_net.call_count, 1) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_cert_not_found(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -141,7 +141,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_net.call_count, 1) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_menu(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -165,7 +165,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_display.more_info_cert.call_count, 1) @mock.patch("letsencrypt.revoker.logging") - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log): mock_display().confirm_revocation.return_value = True From a278d53f5200086c2436ccf56675dd718eb6c4c9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 15:00:18 +0000 Subject: [PATCH 073/109] Rename messages2 to messages. --- acme/{messages2.py => messages.py} | 16 ++--- acme/{messages2_test.py => messages_test.py} | 62 +++++++++---------- docs/pkgs/acme/index.rst | 9 --- examples/restified.py | 8 +-- letsencrypt/account.py | 8 +-- letsencrypt/achallenges.py | 4 +- letsencrypt/auth_handler.py | 24 +++---- letsencrypt/network.py | 48 +++++++------- letsencrypt/tests/account_test.py | 6 +- letsencrypt/tests/acme_util.py | 30 ++++----- letsencrypt/tests/auth_handler_test.py | 42 ++++++------- letsencrypt/tests/network_test.py | 34 +++++----- letsencrypt/tests/proof_of_possession_test.py | 10 +-- letsencrypt_nginx/tests/configurator_test.py | 10 +-- 14 files changed, 151 insertions(+), 160 deletions(-) rename acme/{messages2.py => messages.py} (96%) rename acme/{messages2_test.py => messages_test.py} (83%) diff --git a/acme/messages2.py b/acme/messages.py similarity index 96% rename from acme/messages2.py rename to acme/messages.py index 15b4521de..aa041caed 100644 --- a/acme/messages2.py +++ b/acme/messages.py @@ -100,7 +100,7 @@ IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder class Identifier(jose.JSONObjectWithFields): """ACME identifier. - :ivar acme.messages2.IdentifierType typ: + :ivar acme.messages.IdentifierType typ: """ typ = jose.Field('type', decoder=IdentifierType.from_json) @@ -110,7 +110,7 @@ class Identifier(jose.JSONObjectWithFields): class Resource(jose.ImmutableMap): """ACME Resource. - :ivar acme.messages2.ResourceBody body: Resource body. + :ivar acme.messages.ResourceBody body: Resource body. :ivar str uri: Location of the resource. """ @@ -124,7 +124,7 @@ class ResourceBody(jose.JSONObjectWithFields): class RegistrationResource(Resource): """Registration Resource. - :ivar acme.messages2.Registration body: + :ivar acme.messages.Registration body: :ivar str new_authzr_uri: URI found in the 'next' ``Link`` header :ivar str terms_of_service: URL for the CA TOS. @@ -150,7 +150,7 @@ class Registration(ResourceBody): class ChallengeResource(Resource, jose.JSONObjectWithFields): """Challenge Resource. - :ivar acme.messages2.ChallengeBody body: + :ivar acme.messages.ChallengeBody body: :ivar str authzr_uri: URI found in the 'up' ``Link`` header. """ @@ -175,7 +175,7 @@ class ChallengeBody(ResourceBody): :ivar acme.challenges.Challenge: Wrapped challenge. Conveniently, all challenge fields are proxied, i.e. you can call ``challb.x`` to get ``challb.chall.x`` contents. - :ivar acme.messages2.Status status: + :ivar acme.messages.Status status: :ivar datetime.datetime validated: """ @@ -202,7 +202,7 @@ class ChallengeBody(ResourceBody): class AuthorizationResource(Resource): """Authorization Resource. - :ivar acme.messages2.Authorization body: + :ivar acme.messages.Authorization body: :ivar str new_cert_uri: URI found in the 'next' ``Link`` header """ @@ -212,13 +212,13 @@ class AuthorizationResource(Resource): class Authorization(ResourceBody): """Authorization Resource Body. - :ivar acme.messages2.Identifier identifier: + :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). :ivar acme.jose.jwk.JWK key: Public key. :ivar tuple contact: - :ivar acme.messages2.Status status: + :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ diff --git a/acme/messages2_test.py b/acme/messages_test.py similarity index 83% rename from acme/messages2_test.py rename to acme/messages_test.py index 72ffc954a..4f86d7809 100644 --- a/acme/messages2_test.py +++ b/acme/messages_test.py @@ -1,4 +1,4 @@ -"""Tests for acme.messages2.""" +"""Tests for acme.messages.""" import datetime import os import pkg_resources @@ -17,10 +17,10 @@ KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string( class ErrorTest(unittest.TestCase): - """Tests for acme.messages2.Error.""" + """Tests for acme.messages.Error.""" def setUp(self): - from acme.messages2 import Error + from acme.messages import Error self.error = Error(detail='foo', typ='malformed', title='title') self.jobj = {'detail': 'foo', 'title': 'some title'} @@ -32,14 +32,14 @@ class ErrorTest(unittest.TestCase): 'malformed', self.error.from_json(self.error.to_partial_json()).typ) def test_typ_decoder_missing_prefix(self): - from acme.messages2 import Error + from acme.messages import Error self.jobj['type'] = 'malformed' self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) self.jobj['type'] = 'not valid bare type' self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) def test_typ_decoder_not_recognized(self): - from acme.messages2 import Error + from acme.messages import Error self.jobj['type'] = 'urn:acme:error:baz' self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) @@ -48,7 +48,7 @@ class ErrorTest(unittest.TestCase): 'The request message was malformed', self.error.description) def test_from_json_hashable(self): - from acme.messages2 import Error + from acme.messages import Error hash(Error.from_json(self.error.to_json())) def test_str(self): @@ -59,10 +59,10 @@ class ErrorTest(unittest.TestCase): class ConstantTest(unittest.TestCase): - """Tests for acme.messages2._Constant.""" + """Tests for acme.messages._Constant.""" def setUp(self): - from acme.messages2 import _Constant + from acme.messages import _Constant class MockConstant(_Constant): # pylint: disable=missing-docstring POSSIBLE_NAMES = {} @@ -95,7 +95,7 @@ class ConstantTest(unittest.TestCase): self.assertFalse(self.const_a != const_a_prime) class RegistrationTest(unittest.TestCase): - """Tests for acme.messages2.Registration.""" + """Tests for acme.messages.Registration.""" def setUp(self): key = jose.jwk.JWKRSA(key=KEY.publickey()) @@ -103,7 +103,7 @@ class RegistrationTest(unittest.TestCase): recovery_token = 'XYZ' agreement = 'https://letsencrypt.org/terms' - from acme.messages2 import Registration + from acme.messages import Registration self.reg = Registration( key=key, contact=contact, recovery_token=recovery_token, agreement=agreement) @@ -121,31 +121,31 @@ class RegistrationTest(unittest.TestCase): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) def test_from_json(self): - from acme.messages2 import Registration + from acme.messages import Registration self.assertEqual(self.reg, Registration.from_json(self.jobj_from)) def test_from_json_hashable(self): - from acme.messages2 import Registration + from acme.messages import Registration hash(Registration.from_json(self.jobj_from)) class ChallengeResourceTest(unittest.TestCase): - """Tests for acme.messages2.ChallengeResource.""" + """Tests for acme.messages.ChallengeResource.""" def test_uri(self): - from acme.messages2 import ChallengeResource + from acme.messages import ChallengeResource self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock( uri='http://challb'), authzr_uri='http://authz').uri) class ChallengeBodyTest(unittest.TestCase): - """Tests for acme.messages2.ChallengeBody.""" + """Tests for acme.messages.ChallengeBody.""" def setUp(self): self.chall = challenges.DNS(token='foo') - from acme.messages2 import ChallengeBody - from acme.messages2 import STATUS_VALID + from acme.messages import ChallengeBody + from acme.messages import STATUS_VALID self.status = STATUS_VALID self.challb = ChallengeBody( uri='http://challb', chall=self.chall, status=self.status) @@ -163,11 +163,11 @@ class ChallengeBodyTest(unittest.TestCase): self.assertEqual(self.jobj_to, self.challb.to_partial_json()) def test_from_json(self): - from acme.messages2 import ChallengeBody + from acme.messages import ChallengeBody self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from)) def test_from_json_hashable(self): - from acme.messages2 import ChallengeBody + from acme.messages import ChallengeBody hash(ChallengeBody.from_json(self.jobj_from)) def test_proxy(self): @@ -175,11 +175,11 @@ class ChallengeBodyTest(unittest.TestCase): class AuthorizationTest(unittest.TestCase): - """Tests for acme.messages2.Authorization.""" + """Tests for acme.messages.Authorization.""" def setUp(self): - from acme.messages2 import ChallengeBody - from acme.messages2 import STATUS_VALID + from acme.messages import ChallengeBody + from acme.messages import STATUS_VALID self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, @@ -191,9 +191,9 @@ class AuthorizationTest(unittest.TestCase): ) combinations = ((0, 2), (1, 2)) - from acme.messages2 import Authorization - from acme.messages2 import Identifier - from acme.messages2 import IDENTIFIER_FQDN + from acme.messages import Authorization + from acme.messages import Identifier + from acme.messages import IDENTIFIER_FQDN identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') self.authz = Authorization( identifier=identifier, combinations=combinations, @@ -206,11 +206,11 @@ class AuthorizationTest(unittest.TestCase): } def test_from_json(self): - from acme.messages2 import Authorization + from acme.messages import Authorization Authorization.from_json(self.jobj_from) def test_from_json_hashable(self): - from acme.messages2 import Authorization + from acme.messages import Authorization hash(Authorization.from_json(self.jobj_from)) def test_resolved_combinations(self): @@ -221,10 +221,10 @@ class AuthorizationTest(unittest.TestCase): class RevocationTest(unittest.TestCase): - """Tests for acme.messages2.RevocationTest.""" + """Tests for acme.messages.RevocationTest.""" def setUp(self): - from acme.messages2 import Revocation + from acme.messages import Revocation self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW) self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime( 2015, 3, 27, tzinfo=pytz.utc)) @@ -233,7 +233,7 @@ class RevocationTest(unittest.TestCase): 'revoke': '2015-03-27T00:00:00Z'} def test_revoke_decoder(self): - from acme.messages2 import Revocation + from acme.messages import Revocation self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now)) self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date)) @@ -242,7 +242,7 @@ class RevocationTest(unittest.TestCase): self.assertEqual(self.jobj_date, self.rev_date.to_partial_json()) def test_from_json_hashable(self): - from acme.messages2 import Revocation + from acme.messages import Revocation hash(Revocation.from_json(self.rev_now.to_json())) diff --git a/docs/pkgs/acme/index.rst b/docs/pkgs/acme/index.rst index 1c73a4a42..ea0743b1e 100644 --- a/docs/pkgs/acme/index.rst +++ b/docs/pkgs/acme/index.rst @@ -10,18 +10,9 @@ Messages -------- -v00 -~~~ - .. automodule:: acme.messages :members: -v02 -~~~ - -.. automodule:: acme.messages2 - :members: - Challenges ---------- diff --git a/examples/restified.py b/examples/restified.py index 07c773575..cfd7fa8dd 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -4,7 +4,7 @@ import pkg_resources import M2Crypto -from acme import messages2 +from acme import messages from acme import jose from letsencrypt import network @@ -27,8 +27,8 @@ net.update_registration(regr.update( logging.debug(regr) authzr = net.request_challenges( - identifier=messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value='example1.com'), + identifier=messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example1.com'), new_authzr_uri=regr.new_authzr_uri) logging.debug(authzr) @@ -38,5 +38,5 @@ csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( 'letsencrypt.tests', os.path.join('testdata', 'csr.pem'))) try: net.request_issuance(csr, (authzr,)) -except messages2.Error as error: +except messages.Error as error: print error.detail diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 93a949050..9f351387f 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -6,7 +6,7 @@ import re import configobj import zope.component -from acme import messages2 +from acme import messages from letsencrypt import crypto_util from letsencrypt import errors @@ -28,7 +28,7 @@ class Account(object): :ivar str phone: Client's phone number :ivar regr: Registration Resource - :type regr: :class:`~acme.messages2.RegistrationResource` + :type regr: :class:`~acme.messages.RegistrationResource` """ @@ -141,11 +141,11 @@ class Account(object): if "RegistrationResource" in acc_config: acc_config_rr = acc_config["RegistrationResource"] - regr = messages2.RegistrationResource( + regr = messages.RegistrationResource( uri=acc_config_rr["uri"], new_authzr_uri=acc_config_rr["new_authzr_uri"], terms_of_service=acc_config_rr["terms_of_service"], - body=messages2.Registration.from_json(acc_config_rr["body"])) + body=messages.Registration.from_json(acc_config_rr["body"])) else: regr = None diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 46ef167e0..88dcdbe11 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -5,11 +5,11 @@ Please use names such as ``achall`` to distiguish from variables "of type" and :class:`.ChallengeBody` (denoted by ``challb``):: from acme import challenges - from acme import messages2 + from acme import messages from letsencrypt import achallenges chall = challenges.DNS(token='foo') - challb = messages2.ChallengeBody(chall=chall) + challb = messages.ChallengeBody(chall=chall) achall = achallenges.DNS(chall=challb, domain='example.com') Note, that all annotated challenges act as a proxy objects:: diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index d801613c5..d895c165c 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -4,7 +4,7 @@ import logging import time from acme import challenges -from acme import messages2 +from acme import messages from letsencrypt import achallenges from letsencrypt import constants @@ -30,7 +30,7 @@ class AuthHandler(object): :type account: :class:`letsencrypt.account.Account` :ivar dict authzr: ACME Authorization Resource dict where keys are domains - and values are :class:`acme.messages2.AuthorizationResource` + and values are :class:`acme.messages.AuthorizationResource` :ivar list dv_c: DV challenges in the form of :class:`letsencrypt.achallenges.AnnotatedChallenge` :ivar list cont_c: Continuity challenges in the @@ -82,7 +82,7 @@ class AuthHandler(object): self.verify_authzr_complete() # Only return valid authorizations return [authzr for authzr in self.authzr.values() - if authzr.body.status == messages2.STATUS_VALID] + if authzr.body.status == messages.STATUS_VALID] def _choose_challenges(self, domains): """Retrieve necessary challenges to satisfy server.""" @@ -198,7 +198,7 @@ class AuthHandler(object): failed = [] self.authzr[domain], _ = self.network.poll(self.authzr[domain]) - if self.authzr[domain].body.status == messages2.STATUS_VALID: + if self.authzr[domain].body.status == messages.STATUS_VALID: return achalls, [] # Note: if the whole authorization is invalid, the individual failed @@ -207,9 +207,9 @@ class AuthHandler(object): status = self._get_chall_status(self.authzr[domain], achall) # This does nothing for challenges that have yet to be decided yet. - if status == messages2.STATUS_VALID: + if status == messages.STATUS_VALID: completed.append(achall) - elif status == messages2.STATUS_INVALID: + elif status == messages.STATUS_INVALID: failed.append(achall) return completed, failed @@ -221,7 +221,7 @@ class AuthHandler(object): each challenge resource. :param authzr: Authorization Resource - :type authzr: :class:`acme.messages2.AuthorizationResource` + :type authzr: :class:`acme.messages.AuthorizationResource` :param achall: Annotated challenge for which to get status :type achall: :class:`letsencrypt.achallenges.AnnotatedChallenge` @@ -279,8 +279,8 @@ class AuthHandler(object): """ for authzr in self.authzr.values(): - if (authzr.body.status != messages2.STATUS_VALID and - authzr.body.status != messages2.STATUS_INVALID): + if (authzr.body.status != messages.STATUS_VALID and + authzr.body.status != messages.STATUS_INVALID): raise errors.AuthorizationError("Incomplete authorizations") def _challenge_factory(self, domain, path): @@ -321,7 +321,7 @@ def challb_to_achall(challb, key, domain): """Converts a ChallengeBody object to an AnnotatedChallenge. :param challb: ChallengeBody - :type challb: :class:`acme.messages2.ChallengeBody` + :type challb: :class:`acme.messages.ChallengeBody` :param key: Key :type key: :class:`letsencrypt.le_util.Key` @@ -370,8 +370,8 @@ def gen_challenge_path(challbs, preferences, combinations): .. todo:: This can be possibly be rewritten to use resolved_combinations. :param tuple challbs: A tuple of challenges - (:class:`acme.messages2.Challenge`) from - :class:`acme.messages2.AuthorizationResource` to be + (:class:`acme.messages.Challenge`) from + :class:`acme.messages.AuthorizationResource` to be fulfilled by the client in order to prove possession of the identifier. diff --git a/letsencrypt/network.py b/letsencrypt/network.py index a20194a79..6d3be1afc 100644 --- a/letsencrypt/network.py +++ b/letsencrypt/network.py @@ -11,7 +11,7 @@ import werkzeug from acme import jose from acme import jws as acme_jws -from acme import messages2 +from acme import messages from letsencrypt import errors @@ -75,7 +75,7 @@ class Network(object): function will raise an error. Otherwise, wrong Content-Type is ignored, but logged. - :raises letsencrypt.messages2.Error: If server response body + :raises letsencrypt.messages.Error: If server response body carries HTTP Problem (draft-ietf-appsawg-http-problem-00). :raises letsencrypt.errors.NetworkError: In case of other networking errors. @@ -98,7 +98,7 @@ class Network(object): try: logging.error("Error: %s", jobj) logging.error("Response from server: %s", response.content) - raise messages2.Error.from_json(jobj) + raise messages.Error.from_json(jobj) except jose.DeserializationError as error: # Couldn't deserialize JSON object raise errors.NetworkError((response, error)) @@ -160,7 +160,7 @@ class Network(object): :param JSONDeSerializable obj: Will be wrapped in JWS. :param str content_type: Expected ``Content-Type``, fails if not set. - :raises acme.messages2.NetworkError: + :raises acme.messages.NetworkError: :returns: HTTP Response :rtype: `requests.Response` @@ -192,13 +192,13 @@ class Network(object): except KeyError: raise errors.NetworkError('"next" link missing') - return messages2.RegistrationResource( - body=messages2.Registration.from_json(response.json()), + return messages.RegistrationResource( + body=messages.Registration.from_json(response.json()), uri=response.headers.get('Location', uri), new_authzr_uri=new_authzr_uri, terms_of_service=terms_of_service) - def register(self, contact=messages2.Registration._fields[ + def register(self, contact=messages.Registration._fields[ 'contact'].default): """Register. @@ -211,7 +211,7 @@ class Network(object): :raises letsencrypt.errors.UnexpectedUpdate: """ - new_reg = messages2.Registration(contact=contact) + new_reg = messages.Registration(contact=contact) response = self._post(self.new_reg_uri, new_reg) assert response.status_code == httplib.CREATED # TODO: handle errors @@ -289,8 +289,8 @@ class Network(object): except KeyError: raise errors.NetworkError('"next" link missing') - authzr = messages2.AuthorizationResource( - body=messages2.Authorization.from_json(response.json()), + authzr = messages.AuthorizationResource( + body=messages.Authorization.from_json(response.json()), uri=response.headers.get('Location', uri), new_cert_uri=new_cert_uri) if authzr.body.identifier != identifier: @@ -301,7 +301,7 @@ class Network(object): """Request challenges. :param identifier: Identifier to be challenged. - :type identifier: `.messages2.Identifier` + :type identifier: `.messages.Identifier` :param str new_authzr_uri: new-authorization URI @@ -309,7 +309,7 @@ class Network(object): :rtype: `.AuthorizationResource` """ - new_authz = messages2.Authorization(identifier=identifier) + new_authz = messages.Authorization(identifier=identifier) response = self._post(new_authzr_uri, new_authz) assert response.status_code == httplib.CREATED # TODO: handle errors return self._authzr_from_response(response, identifier) @@ -328,8 +328,8 @@ class Network(object): :rtype: `.AuthorizationResource` """ - return self.request_challenges(messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri) + return self.request_challenges(messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri) def answer_challenge(self, challb, response): """Answer challenge. @@ -351,9 +351,9 @@ class Network(object): authzr_uri = response.links['up']['url'] except KeyError: raise errors.NetworkError('"up" Link header missing') - challr = messages2.ChallengeResource( + challr = messages.ChallengeResource( authzr_uri=authzr_uri, - body=messages2.ChallengeBody.from_json(response.json())) + body=messages.ChallengeBody.from_json(response.json())) # TODO: check that challr.uri == response.headers['Location']? if challr.uri != challb.uri: raise errors.UnexpectedUpdate(challr.uri) @@ -412,14 +412,14 @@ class Network(object): :param authzrs: `list` of `.AuthorizationResource` :returns: Issued certificate - :rtype: `.messages2.CertificateResource` + :rtype: `.messages.CertificateResource` """ assert authzrs, "Authorizations list is empty" logging.debug("Requesting issuance...") # TODO: assert len(authzrs) == number of SANs - req = messages2.CertificateRequest( + req = messages.CertificateRequest( csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument @@ -436,7 +436,7 @@ class Network(object): except KeyError: raise errors.NetworkError('"Location" Header missing') - return messages2.CertificateResource( + return messages.CertificateResource( uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, body=jose.ComparableX509( M2Crypto.X509.load_cert_der_string(response.content))) @@ -459,7 +459,7 @@ class Network(object): ``Retry-After`` is not present in the response. :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is - the issued certificate (`.messages2.CertificateResource.), + the issued certificate (`.messages.CertificateResource.), and ``updated_authzrs`` is a `tuple` consisting of updated Authorization Resources (`.AuthorizationResource`) as present in the responses from server, and in the same order @@ -488,7 +488,7 @@ class Network(object): updated_authzr, response = self.poll(updated[authzr]) updated[authzr] = updated_authzr - if updated_authzr.body.status != messages2.STATUS_VALID: + if updated_authzr.body.status != messages.STATUS_VALID: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( response, default=mintime), authzr)) @@ -561,20 +561,20 @@ class Network(object): else: return None - def revoke(self, certr, when=messages2.Revocation.NOW): + def revoke(self, certr, when=messages.Revocation.NOW): """Revoke certificate. :param certr: Certificate Resource :type certr: `.CertificateResource` :param when: When should the revocation take place? Takes - the same values as `.messages2.Revocation.revoke`. + the same values as `.messages.Revocation.revoke`. :raises letsencrypt.errors.NetworkError: If revocation is unsuccessful. """ - rev = messages2.Revocation(revoke=when, authorizations=tuple( + rev = messages.Revocation(revoke=when, authorizations=tuple( authzr.uri for authzr in certr.authzrs)) response = self._post(certr.uri, rev) if response.status_code != httplib.OK: diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index d14610252..6e9966a55 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -7,7 +7,7 @@ import shutil import tempfile import unittest -from acme import messages2 +from acme import messages from letsencrypt import configuration from letsencrypt import errors @@ -40,11 +40,11 @@ class AccountTest(unittest.TestCase): self.key = le_util.Key(key_file, key_pem) self.email = "client@letsencrypt.org" - self.regr = messages2.RegistrationResource( + self.regr = messages.RegistrationResource( uri="uri", new_authzr_uri="new_authzr_uri", terms_of_service="terms_of_service", - body=messages2.Registration( + body=messages.Registration( recovery_token="recovery_token", agreement="agreement") ) diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index daf651059..7ac05c1fa 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -8,7 +8,7 @@ import Crypto.PublicKey.RSA from acme import challenges from acme import jose -from acme import messages2 +from acme import messages KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( @@ -78,19 +78,19 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name "status": status, } - if status == messages2.STATUS_VALID: + if status == messages.STATUS_VALID: kwargs.update({"validated": datetime.datetime.now()}) - return messages2.ChallengeBody(**kwargs) # pylint: disable=star-args + return messages.ChallengeBody(**kwargs) # pylint: disable=star-args # Pending ChallengeBody objects -DVSNI_P = chall_to_challb(DVSNI, messages2.STATUS_PENDING) -SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages2.STATUS_PENDING) -DNS_P = chall_to_challb(DNS, messages2.STATUS_PENDING) -RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages2.STATUS_PENDING) -RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages2.STATUS_PENDING) -POP_P = chall_to_challb(POP, messages2.STATUS_PENDING) +DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) +SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages.STATUS_PENDING) +DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) +RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING) +RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages.STATUS_PENDING) +POP_P = chall_to_challb(POP, messages.STATUS_PENDING) CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P] @@ -106,7 +106,7 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True): """Generate an authorization resource. :param authz_status: Status object - :type authz_status: :class:`acme.messages2.Status` + :type authz_status: :class:`acme.messages.Status` :param list challs: Challenge objects :param list statuses: status of each challenge object :param bool combos: Whether or not to add combinations @@ -118,13 +118,13 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True): for chall, status in itertools.izip(challs, statuses) ) authz_kwargs = { - "identifier": messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value=domain), + "identifier": messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value=domain), "challenges": challbs, } if combos: authz_kwargs.update({"combinations": gen_combos(challbs)}) - if authz_status == messages2.STATUS_VALID: + if authz_status == messages.STATUS_VALID: authz_kwargs.update({ "status": authz_status, "expires": datetime.datetime.now() + datetime.timedelta(days=31), @@ -135,8 +135,8 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True): }) # pylint: disable=star-args - return messages2.AuthorizationResource( + return messages.AuthorizationResource( uri="https://trusted.ca/new-authz-resource", new_cert_uri="https://trusted.ca/new-cert", - body=messages2.Authorization(**authz_kwargs) + body=messages.Authorization(**authz_kwargs) ) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index fddf508b2..72fba1d0b 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -6,7 +6,7 @@ import unittest import mock from acme import challenges -from acme import messages2 +from acme import messages from letsencrypt import errors from letsencrypt import le_util @@ -37,8 +37,8 @@ class ChallengeFactoryTest(unittest.TestCase): self.dom = "test" self.handler.authzr[self.dom] = acme_util.gen_authzr( - messages2.STATUS_PENDING, self.dom, acme_util.CHALLENGES, - [messages2.STATUS_PENDING]*6, False) + messages.STATUS_PENDING, self.dom, acme_util.CHALLENGES, + [messages.STATUS_PENDING]*6, False) def test_all(self): cont_c, dv_c = self.handler._challenge_factory(self.dom, range(0, 6)) @@ -57,9 +57,9 @@ class ChallengeFactoryTest(unittest.TestCase): def test_unrecognized(self): self.handler.authzr["failure.com"] = acme_util.gen_authzr( - messages2.STATUS_PENDING, "failure.com", + messages.STATUS_PENDING, "failure.com", [mock.Mock(chall="chall", typ="unrecognized")], - [messages2.STATUS_PENDING]) + [messages.STATUS_PENDING]) self.assertRaises(errors.LetsEncryptClientError, self.handler._challenge_factory, "failure.com", [0]) @@ -160,10 +160,10 @@ class GetAuthorizationsTest(unittest.TestCase): for dom in self.handler.authzr.keys(): azr = self.handler.authzr[dom] self.handler.authzr[dom] = acme_util.gen_authzr( - messages2.STATUS_VALID, + messages.STATUS_VALID, dom, [challb.chall for challb in azr.body.challenges], - [messages2.STATUS_VALID]*len(azr.body.challenges), + [messages.STATUS_VALID]*len(azr.body.challenges), azr.body.combinations) @@ -182,16 +182,16 @@ class PollChallengesTest(unittest.TestCase): self.doms = ["0", "1", "2"] self.handler.authzr[self.doms[0]] = acme_util.gen_authzr( - messages2.STATUS_PENDING, self.doms[0], - acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) + messages.STATUS_PENDING, self.doms[0], + acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False) self.handler.authzr[self.doms[1]] = acme_util.gen_authzr( - messages2.STATUS_PENDING, self.doms[1], - acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) + messages.STATUS_PENDING, self.doms[1], + acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False) self.handler.authzr[self.doms[2]] = acme_util.gen_authzr( - messages2.STATUS_PENDING, self.doms[2], - acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) + messages.STATUS_PENDING, self.doms[2], + acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False) self.chall_update = {} for dom in self.doms: @@ -205,7 +205,7 @@ class PollChallengesTest(unittest.TestCase): self.handler._poll_challenges(self.chall_update, False) for authzr in self.handler.authzr.values(): - self.assertEqual(authzr.body.status, messages2.STATUS_VALID) + self.assertEqual(authzr.body.status, messages.STATUS_VALID) @mock.patch("letsencrypt.auth_handler.time") def test_poll_challenges_failure_best_effort(self, unused_mock_time): @@ -213,7 +213,7 @@ class PollChallengesTest(unittest.TestCase): self.handler._poll_challenges(self.chall_update, True) for authzr in self.handler.authzr.values(): - self.assertEqual(authzr.body.status, messages2.STATUS_PENDING) + self.assertEqual(authzr.body.status, messages.STATUS_PENDING) @mock.patch("letsencrypt.auth_handler.time") def test_poll_challenges_failure(self, unused_mock_time): @@ -241,10 +241,10 @@ class PollChallengesTest(unittest.TestCase): # Basically it didn't raise an error and it stopped earlier than # Making all challenges invalid which would make mock_poll_solve_one # change authzr to invalid - return self._mock_poll_solve_one_chall(authzr, messages2.STATUS_VALID) + return self._mock_poll_solve_one_chall(authzr, messages.STATUS_VALID) def _mock_poll_solve_one_invalid(self, authzr): - return self._mock_poll_solve_one_chall(authzr, messages2.STATUS_INVALID) + return self._mock_poll_solve_one_chall(authzr, messages.STATUS_INVALID) def _mock_poll_solve_one_chall(self, authzr, desired_status): # pylint: disable=no-self-use @@ -269,10 +269,10 @@ class PollChallengesTest(unittest.TestCase): else: status_ = authzr.body.status - new_authzr = messages2.AuthorizationResource( + new_authzr = messages.AuthorizationResource( uri=authzr.uri, new_cert_uri=authzr.new_cert_uri, - body=messages2.Authorization( + body=messages.Authorization( identifier=authzr.body.identifier, challenges=new_challbs, combinations=authzr.body.combinations, @@ -429,8 +429,8 @@ def gen_auth_resp(chall_list): def gen_dom_authzr(domain, unused_new_authzr_uri, challs): """Generates new authzr for domains.""" return acme_util.gen_authzr( - messages2.STATUS_PENDING, domain, challs, - [messages2.STATUS_PENDING]*len(challs)) + messages.STATUS_PENDING, domain, challs, + [messages.STATUS_PENDING]*len(challs)) if __name__ == "__main__": diff --git a/letsencrypt/tests/network_test.py b/letsencrypt/tests/network_test.py index c0522c2fb..586dc7ecb 100644 --- a/letsencrypt/tests/network_test.py +++ b/letsencrypt/tests/network_test.py @@ -14,7 +14,7 @@ import requests from acme import challenges from acme import jose from acme import jws as acme_jws -from acme import messages2 +from acme import messages from letsencrypt import account from letsencrypt import errors @@ -58,37 +58,37 @@ class NetworkTest(unittest.TestCase): self.post = mock.MagicMock(return_value=self.response) self.get = mock.MagicMock(return_value=self.response) - self.identifier = messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value='example.com') + self.identifier = messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example.com') self.config = mock.Mock(accounts_dir=tempfile.mkdtemp()) # Registration self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') - reg = messages2.Registration( + reg = messages.Registration( contact=self.contact, key=KEY.public(), recovery_token='t') - self.regr = messages2.RegistrationResource( + self.regr = messages.RegistrationResource( body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', terms_of_service='https://www.letsencrypt-demo.org/tos') # Authorization authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' - challb = messages2.ChallengeBody( - uri=(authzr_uri + '/1'), status=messages2.STATUS_VALID, + challb = messages.ChallengeBody( + uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, chall=challenges.DNS(token='foo')) - self.challr = messages2.ChallengeResource( + self.challr = messages.ChallengeResource( body=challb, authzr_uri=authzr_uri) - self.authz = messages2.Authorization( - identifier=messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value='example.com'), + self.authz = messages.Authorization( + identifier=messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example.com'), challenges=(challb,), combinations=None) - self.authzr = messages2.AuthorizationResource( + self.authzr = messages.AuthorizationResource( body=self.authz, uri=authzr_uri, new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') # Request issuance - self.certr = messages2.CertificateResource( + self.certr = messages.CertificateResource( body=CERT, authzrs=(self.authzr,), uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') @@ -131,11 +131,11 @@ class NetworkTest(unittest.TestCase): def test_check_response_not_ok_jobj_error(self): self.response.ok = False - self.response.json.return_value = messages2.Error( + self.response.json.return_value = messages.Error( detail='foo', typ='serverInternal', title='some title').to_json() # pylint: disable=protected-access self.assertRaises( - messages2.Error, self.net._check_response, self.response) + messages.Error, self.net._check_response, self.response) def test_check_response_not_ok_no_jobj(self): self.response.ok = False @@ -462,7 +462,7 @@ class NetworkTest(unittest.TestCase): if not authzr.retries: # no more retries done = mock.MagicMock(uri=authzr.uri, times=authzr.times) - done.body.status = messages2.STATUS_VALID + done.body.status = messages.STATUS_VALID return done, [] # response (2nd result tuple element) is reduced to only @@ -550,7 +550,7 @@ class NetworkTest(unittest.TestCase): def test_revoke(self): self._mock_post_get() - self.net.revoke(self.certr, when=messages2.Revocation.NOW) + self.net.revoke(self.certr, when=messages.Revocation.NOW) self.post.assert_called_once_with(self.certr.uri, mock.ANY) def test_revoke_bad_status_raises_error(self): diff --git a/letsencrypt/tests/proof_of_possession_test.py b/letsencrypt/tests/proof_of_possession_test.py index 0a044810c..415e4caed 100644 --- a/letsencrypt/tests/proof_of_possession_test.py +++ b/letsencrypt/tests/proof_of_possession_test.py @@ -8,7 +8,7 @@ import mock from acme import challenges from acme import jose -from acme import messages2 +from acme import messages from letsencrypt import achallenges from letsencrypt import proof_of_possession @@ -48,8 +48,8 @@ class ProofOfPossessionTest(unittest.TestCase): issuers=(), authorized_for=()) chall = challenges.ProofOfPossession( alg=jose.RS256, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) - challb = messages2.ChallengeBody( - chall=chall, uri="http://example", status=messages2.STATUS_PENDING) + challb = messages.ChallengeBody( + chall=chall, uri="http://example", status=messages.STATUS_PENDING) self.achall = achallenges.ProofOfPossession( challb=challb, domain="example.com") @@ -60,8 +60,8 @@ class ProofOfPossessionTest(unittest.TestCase): issuers=(), authorized_for=()) chall = challenges.ProofOfPossession( alg=jose.HS512, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) - challb = messages2.ChallengeBody( - chall=chall, uri="http://example", status=messages2.STATUS_PENDING) + challb = messages.ChallengeBody( + chall=chall, uri="http://example", status=messages.STATUS_PENDING) self.achall = achallenges.ProofOfPossession( challb=challb, domain="example.com") self.assertEqual(self.proof_of_pos.perform(self.achall), None) diff --git a/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt_nginx/tests/configurator_test.py index 82b80b9d2..94a0901b5 100644 --- a/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt_nginx/tests/configurator_test.py @@ -5,7 +5,7 @@ import unittest import mock from acme import challenges -from acme import messages2 +from acme import messages from letsencrypt import achallenges from letsencrypt import errors @@ -165,20 +165,20 @@ class NginxConfiguratorTest(util.NginxTest): # Note: As more challenges are offered this will have to be expanded auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) achall1 = achallenges.DVSNI( - challb=messages2.ChallengeBody( + challb=messages.ChallengeBody( chall=challenges.DVSNI( r="foo", nonce="bar"), uri="https://ca.org/chall0_uri", - status=messages2.Status("pending"), + status=messages.Status("pending"), ), domain="localhost", key=auth_key) achall2 = achallenges.DVSNI( - challb=messages2.ChallengeBody( + challb=messages.ChallengeBody( chall=challenges.DVSNI( r="abc", nonce="def"), uri="https://ca.org/chall1_uri", - status=messages2.Status("pending"), + status=messages.Status("pending"), ), domain="example.com", key=auth_key) dvsni_ret_val = [ From b4d63cbbb3e2823ab6b422b3c08b70f310fe4a51 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 10:38:20 +0000 Subject: [PATCH 074/109] Move letsencrypt.network to acme.client. --- acme/client.py | 559 +++++++++++++++++++++++++++++ acme/client_test.py | 530 ++++++++++++++++++++++++++++ acme/errors.py | 7 + acme/jose/testdata/README | 9 +- acme/jose/testdata/cert.der | Bin 0 -> 377 bytes acme/jose/testdata/csr.der | Bin 0 -> 210 bytes acme/jose/testdata/csr2.pem | 10 - docs/pkgs/acme/index.rst | 7 + letsencrypt/errors.py | 8 - letsencrypt/network.py | 568 +----------------------------- letsencrypt/tests/network_test.py | 515 +-------------------------- 11 files changed, 1115 insertions(+), 1098 deletions(-) create mode 100644 acme/client.py create mode 100644 acme/client_test.py create mode 100644 acme/jose/testdata/cert.der create mode 100644 acme/jose/testdata/csr.der delete mode 100644 acme/jose/testdata/csr2.pem diff --git a/acme/client.py b/acme/client.py new file mode 100644 index 000000000..c0eda0fa3 --- /dev/null +++ b/acme/client.py @@ -0,0 +1,559 @@ +"""ACME client API.""" +import datetime +import heapq +import httplib +import logging +import time + +import M2Crypto +import requests +import werkzeug + +from acme import errors +from acme import jose +from acme import jws +from acme import messages + + +# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning +requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() + + +class Client(object): + """ACME client. + + .. todo:: + Clean up raised error types hierarchy, document, and handle (wrap) + instances of `.DeserializationError` raised in `from_json()`. + + :ivar str new_reg_uri: Location of new-reg + :ivar key: `.JWK` (private) + :ivar alg: `.JWASignature` + :ivar bool verify_ssl: Verify SSL certificates? + + """ + DER_CONTENT_TYPE = 'application/pkix-cert' + JSON_CONTENT_TYPE = 'application/json' + JSON_ERROR_CONTENT_TYPE = 'application/problem+json' + REPLAY_NONCE_HEADER = 'Replay-Nonce' + + def __init__(self, new_reg_uri, key, alg=jose.RS256, verify_ssl=True): + self.new_reg_uri = new_reg_uri + self.key = key + self.alg = alg + self.verify_ssl = verify_ssl + self._nonces = set() + + def _wrap_in_jws(self, obj, nonce): + """Wrap `JSONDeSerializable` object in JWS. + + .. todo:: Implement ``acmePath``. + + :param JSONDeSerializable obj: + :rtype: `.JWS` + + """ + dumps = obj.json_dumps() + logging.debug('Serialized JSON: %s', dumps) + return jws.JWS.sign( + payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() + + @classmethod + def _check_response(cls, response, content_type=None): + """Check response content and its type. + + .. note:: + Checking is not strict: wrong server response ``Content-Type`` + HTTP header is ignored if response is an expected JSON object + (c.f. Boulder #56). + + :param str content_type: Expected Content-Type response header. + If JSON is expected and not present in server response, this + function will raise an error. Otherwise, wrong Content-Type + is ignored, but logged. + + :raises .messages.Error: If server response body + carries HTTP Problem (draft-ietf-appsawg-http-problem-00). + :raises .ClientError: In case of other networking errors. + + """ + response_ct = response.headers.get('Content-Type') + try: + # TODO: response.json() is called twice, once here, and + # once in _get and _post clients + jobj = response.json() + except ValueError as error: + jobj = None + + if not response.ok: + if jobj is not None: + if response_ct != cls.JSON_ERROR_CONTENT_TYPE: + logging.debug( + 'Ignoring wrong Content-Type (%r) for JSON Error', + response_ct) + try: + logging.error("Error: %s", jobj) + logging.error("Response from server: %s", response.content) + raise messages.Error.from_json(jobj) + except jose.DeserializationError as error: + # Couldn't deserialize JSON object + raise errors.ClientError((response, error)) + else: + # response is not JSON object + raise errors.ClientError(response) + else: + if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: + logging.debug( + 'Ignoring wrong Content-Type (%r) for JSON decodable ' + 'response', response_ct) + + if content_type == cls.JSON_CONTENT_TYPE and jobj is None: + raise errors.ClientError( + 'Unexpected response Content-Type: {0}'.format(response_ct)) + + def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs): + """Send GET request. + + :raises .ClientError: + + :returns: HTTP Response + :rtype: `requests.Response` + + """ + logging.debug('Sending GET request to %s', uri) + kwargs.setdefault('verify', self.verify_ssl) + try: + response = requests.get(uri, **kwargs) + except requests.exceptions.RequestException as error: + raise errors.ClientError(error) + self._check_response(response, content_type=content_type) + return response + + def _add_nonce(self, response): + if self.REPLAY_NONCE_HEADER in response.headers: + nonce = response.headers[self.REPLAY_NONCE_HEADER] + error = jws.Header.validate_nonce(nonce) + if error is None: + logging.debug('Storing nonce: %r', nonce) + self._nonces.add(nonce) + else: + raise errors.ClientError('Invalid nonce ({0}): {1}'.format( + nonce, error)) + else: + raise errors.ClientError( + 'Server {0} response did not include a replay nonce'.format( + response.request.method)) + + def _get_nonce(self, uri): + if not self._nonces: + logging.debug('Requesting fresh nonce by sending HEAD to %s', uri) + self._add_nonce(requests.head(uri)) + return self._nonces.pop() + + def _post(self, uri, obj, content_type=JSON_CONTENT_TYPE, **kwargs): + """Send POST data. + + :param JSONDeSerializable obj: Will be wrapped in JWS. + :param str content_type: Expected ``Content-Type``, fails if not set. + + :raises acme.messages.ClientError: + + :returns: HTTP Response + :rtype: `requests.Response` + + """ + data = self._wrap_in_jws(obj, self._get_nonce(uri)) + logging.debug('Sending POST data to %s: %s', uri, data) + kwargs.setdefault('verify', self.verify_ssl) + try: + response = requests.post(uri, data=data, **kwargs) + except requests.exceptions.RequestException as error: + raise errors.ClientError(error) + logging.debug('Received response %s: %r', response, response.text) + + self._add_nonce(response) + self._check_response(response, content_type=content_type) + return response + + @classmethod + def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, + terms_of_service=None): + terms_of_service = ( + response.links['terms-of-service']['url'] + if 'terms-of-service' in response.links else terms_of_service) + + if new_authzr_uri is None: + try: + new_authzr_uri = response.links['next']['url'] + except KeyError: + raise errors.ClientError('"next" link missing') + + return messages.RegistrationResource( + body=messages.Registration.from_json(response.json()), + uri=response.headers.get('Location', uri), + new_authzr_uri=new_authzr_uri, + terms_of_service=terms_of_service) + + def register(self, contact=messages.Registration._fields[ + 'contact'].default): + """Register. + + :param contact: Contact list, as accepted by `.Registration` + :type contact: `tuple` + + :returns: Registration Resource. + :rtype: `.RegistrationResource` + + :raises .UnexpectedUpdate: + + """ + new_reg = messages.Registration(contact=contact) + + response = self._post(self.new_reg_uri, new_reg) + assert response.status_code == httplib.CREATED # TODO: handle errors + + regr = self._regr_from_response(response) + if regr.body.key != self.key.public() or regr.body.contact != contact: + raise errors.UnexpectedUpdate(regr) + + return regr + + def update_registration(self, regr): + """Update registration. + + :pram regr: Registration Resource. + :type regr: `.RegistrationResource` + + :returns: Updated Registration Resource. + :rtype: `.RegistrationResource` + + """ + response = self._post(regr.uri, regr.body) + + # TODO: Boulder returns httplib.ACCEPTED + #assert response.status_code == httplib.OK + + # TODO: Boulder does not set Location or Link on update + # (c.f. acme-spec #94) + + updated_regr = self._regr_from_response( + response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, + terms_of_service=regr.terms_of_service) + if updated_regr != regr: + raise errors.UnexpectedUpdate(regr) + return updated_regr + + def agree_to_tos(self, regr): + """Agree to the terms-of-service. + + Agree to the terms-of-service in a Registration Resource. + + :param regr: Registration Resource. + :type regr: `.RegistrationResource` + + :returns: Updated Registration Resource. + :rtype: `.RegistrationResource` + + """ + return self.update_registration( + regr.update(body=regr.body.update(agreement=regr.terms_of_service))) + + def _authzr_from_response(self, response, identifier, + uri=None, new_cert_uri=None): + # pylint: disable=no-self-use + if new_cert_uri is None: + try: + new_cert_uri = response.links['next']['url'] + except KeyError: + raise errors.ClientError('"next" link missing') + + authzr = messages.AuthorizationResource( + body=messages.Authorization.from_json(response.json()), + uri=response.headers.get('Location', uri), + new_cert_uri=new_cert_uri) + if authzr.body.identifier != identifier: + raise errors.UnexpectedUpdate(authzr) + return authzr + + def request_challenges(self, identifier, new_authzr_uri): + """Request challenges. + + :param identifier: Identifier to be challenged. + :type identifier: `.messages.Identifier` + + :param str new_authzr_uri: new-authorization URI + + :returns: Authorization Resource. + :rtype: `.AuthorizationResource` + + """ + new_authz = messages.Authorization(identifier=identifier) + response = self._post(new_authzr_uri, new_authz) + assert response.status_code == httplib.CREATED # TODO: handle errors + return self._authzr_from_response(response, identifier) + + def request_domain_challenges(self, domain, new_authz_uri): + """Request challenges for domain names. + + This is simply a convenience function that wraps around + `request_challenges`, but works with domain names instead of + generic identifiers. + + :param str domain: Domain name to be challenged. + :param str new_authzr_uri: new-authorization URI + + :returns: Authorization Resource. + :rtype: `.AuthorizationResource` + + """ + return self.request_challenges(messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri) + + def answer_challenge(self, challb, response): + """Answer challenge. + + :param challb: Challenge Resource body. + :type challb: `.ChallengeBody` + + :param response: Corresponding Challenge response + :type response: `.challenges.ChallengeResponse` + + :returns: Challenge Resource with updated body. + :rtype: `.ChallengeResource` + + :raises errors.UnexpectedUpdate: + + """ + response = self._post(challb.uri, response) + try: + authzr_uri = response.links['up']['url'] + except KeyError: + raise errors.ClientError('"up" Link header missing') + challr = messages.ChallengeResource( + authzr_uri=authzr_uri, + body=messages.ChallengeBody.from_json(response.json())) + # TODO: check that challr.uri == response.headers['Location']? + if challr.uri != challb.uri: + raise errors.UnexpectedUpdate(challr.uri) + return challr + + @classmethod + def retry_after(cls, response, default): + """Compute next `poll` time based on response ``Retry-After`` header. + + :param response: Response from `poll`. + :type response: `requests.Response` + + :param int default: Default value (in seconds), used when + ``Retry-After`` header is not present or invalid. + + :returns: Time point when next `poll` should be performed. + :rtype: `datetime.datetime` + + """ + retry_after = response.headers.get('Retry-After', str(default)) + try: + seconds = int(retry_after) + except ValueError: + # pylint: disable=no-member + decoded = werkzeug.parse_date(retry_after) # RFC1123 + if decoded is None: + seconds = default + else: + return decoded + + return datetime.datetime.now() + datetime.timedelta(seconds=seconds) + + def poll(self, authzr): + """Poll Authorization Resource for status. + + :param authzr: Authorization Resource + :type authzr: `.AuthorizationResource` + + :returns: Updated Authorization Resource and HTTP response. + + :rtype: (`.AuthorizationResource`, `requests.Response`) + + """ + response = self._get(authzr.uri) + updated_authzr = self._authzr_from_response( + response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) + # TODO: check and raise UnexpectedUpdate + return updated_authzr, response + + def request_issuance(self, csr, authzrs): + """Request issuance. + + :param csr: CSR + :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` + + :param authzrs: `list` of `.AuthorizationResource` + + :returns: Issued certificate + :rtype: `.messages.CertificateResource` + + """ + assert authzrs, "Authorizations list is empty" + logging.debug("Requesting issuance...") + + # TODO: assert len(authzrs) == number of SANs + req = messages.CertificateRequest( + csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) + + content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument + response = self._post( + authzrs[0].new_cert_uri, # TODO: acme-spec #90 + req, + content_type=content_type, + headers={'Accept': content_type}) + + cert_chain_uri = response.links.get('up', {}).get('url') + + try: + uri = response.headers['Location'] + except KeyError: + raise errors.ClientError('"Location" Header missing') + + return messages.CertificateResource( + uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, + body=jose.ComparableX509( + M2Crypto.X509.load_cert_der_string(response.content))) + + def poll_and_request_issuance(self, csr, authzrs, mintime=5): + """Poll and request issuance. + + This function polls all provided Authorization Resource URIs + until all challenges are valid, respecting ``Retry-After`` HTTP + headers, and then calls `request_issuance`. + + .. todo:: add `max_attempts` or `timeout` + + :param csr: CSR. + :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` + + :param authzrs: `list` of `.AuthorizationResource` + + :param int mintime: Minimum time before next attempt, used if + ``Retry-After`` is not present in the response. + + :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is + the issued certificate (`.messages.CertificateResource.), + and ``updated_authzrs`` is a `tuple` consisting of updated + Authorization Resources (`.AuthorizationResource`) as + present in the responses from server, and in the same order + as the input ``authzrs``. + :rtype: `tuple` + + """ + # priority queue with datetime (based on Retry-After) as key, + # and original Authorization Resource as value + waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs] + # mapping between original Authorization Resource and the most + # recently updated one + updated = dict((authzr, authzr) for authzr in authzrs) + + while waiting: + # find the smallest Retry-After, and sleep if necessary + when, authzr = heapq.heappop(waiting) + now = datetime.datetime.now() + if when > now: + seconds = (when - now).seconds + logging.debug('Sleeping for %d seconds', seconds) + time.sleep(seconds) + + # Note that we poll with the latest updated Authorization + # URI, which might have a different URI than initial one + updated_authzr, response = self.poll(updated[authzr]) + updated[authzr] = updated_authzr + + if updated_authzr.body.status != messages.STATUS_VALID: + # push back to the priority queue, with updated retry_after + heapq.heappush(waiting, (self.retry_after( + response, default=mintime), authzr)) + + updated_authzrs = tuple(updated[authzr] for authzr in authzrs) + return self.request_issuance(csr, updated_authzrs), updated_authzrs + + def _get_cert(self, uri): + """Returns certificate from URI. + + :param str uri: URI of certificate + + :returns: tuple of the form + (response, :class:`acme.jose.ComparableX509`) + :rtype: tuple + + """ + content_type = self.DER_CONTENT_TYPE # TODO: make it a param + response = self._get(uri, headers={'Accept': content_type}, + content_type=content_type) + return response, jose.ComparableX509( + M2Crypto.X509.load_cert_der_string(response.content)) + + def check_cert(self, certr): + """Check for new cert. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :returns: Updated Certificate Resource. + :rtype: `.CertificateResource` + + """ + # TODO: acme-spec 5.1 table action should be renamed to + # "refresh cert", and this method integrated with self.refresh + response, cert = self._get_cert(certr.uri) + if 'Location' not in response.headers: + raise errors.ClientError('Location header missing') + if response.headers['Location'] != certr.uri: + raise errors.UnexpectedUpdate(response.text) + return certr.update(body=cert) + + def refresh(self, certr): + """Refresh certificate. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :returns: Updated Certificate Resource. + :rtype: `.CertificateResource` + + """ + # TODO: If a client sends a refresh request and the server is + # not willing to refresh the certificate, the server MUST + # respond with status code 403 (Forbidden) + return self.check_cert(certr) + + def fetch_chain(self, certr): + """Fetch chain for certificate. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :returns: Certificate chain, or `None` if no "up" Link was provided. + :rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509` + + """ + if certr.cert_chain_uri is not None: + return self._get_cert(certr.cert_chain_uri)[1] + else: + return None + + def revoke(self, certr, when=messages.Revocation.NOW): + """Revoke certificate. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :param when: When should the revocation take place? Takes + the same values as `.messages.Revocation.revoke`. + + :raises .ClientError: If revocation is unsuccessful. + + """ + rev = messages.Revocation(revoke=when, authorizations=tuple( + authzr.uri for authzr in certr.authzrs)) + response = self._post(certr.uri, rev) + if response.status_code != httplib.OK: + raise errors.ClientError( + 'Successful revocation must return HTTP OK status') diff --git a/acme/client_test.py b/acme/client_test.py new file mode 100644 index 000000000..5e4cc1720 --- /dev/null +++ b/acme/client_test.py @@ -0,0 +1,530 @@ +"""Tests for acme.client.""" +import datetime +import httplib +import os +import pkg_resources +import unittest + +import M2Crypto +import mock +import requests + +from acme import challenges +from acme import errors +from acme import jose +from acme import jws as acme_jws +from acme import messages + + +CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( + pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'cert.der')), + M2Crypto.X509.FORMAT_DER)) +CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( + pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'csr.der')), + M2Crypto.X509.FORMAT_DER)) +KEY = jose.JWKRSA.load(pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) +KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'rsa256_key.pem'))) + + +class ClientTest(unittest.TestCase): + """Tests for acme.client.Client.""" + + # pylint: disable=too-many-instance-attributes,too-many-public-methods + + def setUp(self): + self.verify_ssl = mock.MagicMock() + self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) + + from acme.client import Client + self.net = Client( + new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', + key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) + self.nonce = jose.b64encode('Nonce') + self.net._nonces.add(self.nonce) # pylint: disable=protected-access + + self.response = mock.MagicMock(ok=True, status_code=httplib.OK) + self.response.headers = {} + self.response.links = {} + + self.post = mock.MagicMock(return_value=self.response) + self.get = mock.MagicMock(return_value=self.response) + + self.identifier = messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example.com') + + # Registration + self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') + reg = messages.Registration( + contact=self.contact, key=KEY.public(), recovery_token='t') + self.regr = messages.RegistrationResource( + body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', + new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', + terms_of_service='https://www.letsencrypt-demo.org/tos') + + # Authorization + authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' + challb = messages.ChallengeBody( + uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, + chall=challenges.DNS(token='foo')) + self.challr = messages.ChallengeResource( + body=challb, authzr_uri=authzr_uri) + self.authz = messages.Authorization( + identifier=messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example.com'), + challenges=(challb,), combinations=None) + self.authzr = messages.AuthorizationResource( + body=self.authz, uri=authzr_uri, + new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') + + # Request issuance + self.certr = messages.CertificateResource( + body=CERT, authzrs=(self.authzr,), + uri='https://www.letsencrypt-demo.org/acme/cert/1', + cert_chain_uri='https://www.letsencrypt-demo.org/ca') + + def _mock_post_get(self): + # pylint: disable=protected-access + self.net._post = self.post + self.net._get = self.get + + def test_init(self): + self.assertTrue(self.net.verify_ssl is self.verify_ssl) + + def test_wrap_in_jws(self): + class MockJSONDeSerializable(jose.JSONDeSerializable): + # pylint: disable=missing-docstring + def __init__(self, value): + self.value = value + def to_partial_json(self): + return self.value + @classmethod + def from_json(cls, value): + pass # pragma: no cover + # pylint: disable=protected-access + jws_dump = self.net._wrap_in_jws( + MockJSONDeSerializable('foo'), nonce='Tg') + jws = acme_jws.JWS.json_loads(jws_dump) + self.assertEqual(jws.payload, '"foo"') + self.assertEqual(jws.signature.combined.nonce, 'Tg') + # TODO: check that nonce is in protected header + + def test_check_response_not_ok_jobj_no_error(self): + self.response.ok = False + self.response.json.return_value = {} + # pylint: disable=protected-access + self.assertRaises( + errors.ClientError, self.net._check_response, self.response) + + def test_check_response_not_ok_jobj_error(self): + self.response.ok = False + self.response.json.return_value = messages.Error( + detail='foo', typ='serverInternal', title='some title').to_json() + # pylint: disable=protected-access + self.assertRaises( + messages.Error, self.net._check_response, self.response) + + def test_check_response_not_ok_no_jobj(self): + self.response.ok = False + self.response.json.side_effect = ValueError + # pylint: disable=protected-access + self.assertRaises( + errors.ClientError, self.net._check_response, self.response) + + def test_check_response_ok_no_jobj_ct_required(self): + self.response.json.side_effect = ValueError + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.assertRaises( + errors.ClientError, self.net._check_response, self.response, + content_type=self.net.JSON_CONTENT_TYPE) + + def test_check_response_ok_no_jobj_no_ct(self): + self.response.json.side_effect = ValueError + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.net._check_response(self.response) + + def test_check_response_jobj(self): + self.response.json.return_value = {} + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.net._check_response(self.response) + + @mock.patch('acme.client.requests') + def test_get_requests_error_passthrough(self, requests_mock): + requests_mock.exceptions = requests.exceptions + requests_mock.get.side_effect = requests.exceptions.RequestException + # pylint: disable=protected-access + self.assertRaises(errors.ClientError, self.net._get, 'uri') + + @mock.patch('acme.client.requests') + def test_get(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self.net._get('uri', content_type='ct') + self.net._check_response.assert_called_once_with( + requests_mock.get('uri'), content_type='ct') + + def _mock_wrap_in_jws(self): + # pylint: disable=protected-access + self.net._wrap_in_jws = self.wrap_in_jws + + @mock.patch('acme.client.requests') + def test_post_requests_error_passthrough(self, requests_mock): + requests_mock.exceptions = requests.exceptions + requests_mock.post.side_effect = requests.exceptions.RequestException + # pylint: disable=protected-access + self._mock_wrap_in_jws() + self.assertRaises( + errors.ClientError, self.net._post, 'uri', mock.sentinel.obj) + + @mock.patch('acme.client.requests') + def test_post(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self._mock_wrap_in_jws() + requests_mock.post().headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + self.net._post('uri', mock.sentinel.obj, content_type='ct') + self.net._check_response.assert_called_once_with( + requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') + + @mock.patch('acme.client.requests') + def test_post_replay_nonce_handling(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self._mock_wrap_in_jws() + + self.net._nonces.clear() + self.assertRaises( + errors.ClientError, self.net._post, 'uri', mock.sentinel.obj) + + nonce2 = jose.b64encode('Nonce2') + requests_mock.head('uri').headers = { + self.net.REPLAY_NONCE_HEADER: nonce2} + requests_mock.post('uri').headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + + self.net._post('uri', mock.sentinel.obj) + + requests_mock.head.assert_called_with('uri') + self.wrap_in_jws.assert_called_once_with(mock.sentinel.obj, nonce2) + self.assertEqual(self.net._nonces, set([self.nonce])) + + # wrong nonce + requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'} + self.assertRaises( + errors.ClientError, self.net._post, 'uri', mock.sentinel.obj) + + @mock.patch('acme.client.requests') + def test_get_post_verify_ssl(self, requests_mock): + # pylint: disable=protected-access + self._mock_wrap_in_jws() + self.net._check_response = mock.MagicMock() + + for verify_ssl in [True, False]: + self.net.verify_ssl = verify_ssl + self.net._get('uri') + self.net._nonces.add('N') + requests_mock.post().headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + self.net._post('uri', mock.sentinel.obj) + requests_mock.get.assert_called_once_with('uri', verify=verify_ssl) + requests_mock.post.assert_called_with( + 'uri', data=mock.sentinel.wrapped, verify=verify_ssl) + requests_mock.reset_mock() + + def test_register(self): + self.response.status_code = httplib.CREATED + self.response.json.return_value = self.regr.body.to_json() + self.response.headers['Location'] = self.regr.uri + self.response.links.update({ + 'next': {'url': self.regr.new_authzr_uri}, + 'terms-of-service': {'url': self.regr.terms_of_service}, + }) + + self._mock_post_get() + self.assertEqual(self.regr, self.net.register(self.contact)) + # TODO: test POST call arguments + + # TODO: split here and separate test + reg_wrong_key = self.regr.body.update(key=KEY2.public()) + self.response.json.return_value = reg_wrong_key.to_json() + self.assertRaises( + errors.UnexpectedUpdate, self.net.register, self.contact) + + def test_register_missing_next(self): + self.response.status_code = httplib.CREATED + self._mock_post_get() + self.assertRaises( + errors.ClientError, self.net.register, self.regr.body) + + def test_update_registration(self): + self.response.headers['Location'] = self.regr.uri + self.response.json.return_value = self.regr.body.to_json() + self._mock_post_get() + self.assertEqual(self.regr, self.net.update_registration(self.regr)) + + # TODO: split here and separate test + self.response.json.return_value = self.regr.body.update( + contact=()).to_json() + self.assertRaises( + errors.UnexpectedUpdate, self.net.update_registration, self.regr) + + def test_agree_to_tos(self): + self.net.update_registration = mock.Mock() + self.net.agree_to_tos(self.regr) + regr = self.net.update_registration.call_args[0][0] + self.assertEqual(self.regr.terms_of_service, regr.body.agreement) + + def test_request_challenges(self): + self.response.status_code = httplib.CREATED + self.response.headers['Location'] = self.authzr.uri + self.response.json.return_value = self.authz.to_json() + self.response.links = { + 'next': {'url': self.authzr.new_cert_uri}, + } + + self._mock_post_get() + self.net.request_challenges(self.identifier, self.authzr.uri) + # TODO: test POST call arguments + + # TODO: split here and separate test + self.response.json.return_value = self.authz.update( + identifier=self.identifier.update(value='foo')).to_json() + self.assertRaises(errors.UnexpectedUpdate, self.net.request_challenges, + self.identifier, self.authzr.uri) + + def test_request_challenges_missing_next(self): + self.response.status_code = httplib.CREATED + self._mock_post_get() + self.assertRaises( + errors.ClientError, self.net.request_challenges, + self.identifier, self.regr) + + def test_request_domain_challenges(self): + self.net.request_challenges = mock.MagicMock() + self.assertEqual( + self.net.request_challenges(self.identifier), + self.net.request_domain_challenges('example.com', self.regr)) + + def test_answer_challenge(self): + self.response.links['up'] = {'url': self.challr.authzr_uri} + self.response.json.return_value = self.challr.body.to_json() + + chall_response = challenges.DNSResponse() + + self._mock_post_get() + self.net.answer_challenge(self.challr.body, chall_response) + + # TODO: split here and separate test + self.assertRaises(errors.UnexpectedUpdate, self.net.answer_challenge, + self.challr.body.update(uri='foo'), chall_response) + + def test_answer_challenge_missing_next(self): + self._mock_post_get() + self.assertRaises(errors.ClientError, self.net.answer_challenge, + self.challr.body, challenges.DNSResponse()) + + def test_retry_after_date(self): + self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' + self.assertEqual( + datetime.datetime(1999, 12, 31, 23, 59, 59), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_invalid(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = 'foooo' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_seconds(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = '50' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 50), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_missing(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.net.retry_after(response=self.response, default=10)) + + def test_poll(self): + self.response.json.return_value = self.authzr.body.to_json() + self._mock_post_get() + self.assertEqual((self.authzr, self.response), + self.net.poll(self.authzr)) + + # TODO: split here and separate test + self.response.json.return_value = self.authz.update( + identifier=self.identifier.update(value='foo')).to_json() + self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) + + def test_request_issuance(self): + self.response.content = CERT.as_der() + self.response.headers['Location'] = self.certr.uri + self.response.links['up'] = {'url': self.certr.cert_chain_uri} + self._mock_post_get() + self.assertEqual( + self.certr, self.net.request_issuance(CSR, (self.authzr,))) + # TODO: check POST args + + def test_request_issuance_missing_up(self): + self.response.content = CERT.as_der() + self.response.headers['Location'] = self.certr.uri + self._mock_post_get() + self.assertEqual( + self.certr.update(cert_chain_uri=None), + self.net.request_issuance(CSR, (self.authzr,))) + + def test_request_issuance_missing_location(self): + self._mock_post_get() + self.assertRaises( + errors.ClientError, self.net.request_issuance, + CSR, (self.authzr,)) + + @mock.patch('acme.client.datetime') + @mock.patch('acme.client.time') + def test_poll_and_request_issuance(self, time_mock, dt_mock): + # clock.dt | pylint: disable=no-member + clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) + + def sleep(seconds): + """increment clock""" + clock.dt += datetime.timedelta(seconds=seconds) + time_mock.sleep.side_effect = sleep + + def now(): + """return current clock value""" + return clock.dt + dt_mock.datetime.now.side_effect = now + dt_mock.timedelta = datetime.timedelta + + def poll(authzr): # pylint: disable=missing-docstring + # record poll start time based on the current clock value + authzr.times.append(clock.dt) + + # suppose it takes 2 seconds for server to produce the + # result, increment clock + clock.dt += datetime.timedelta(seconds=2) + + if not authzr.retries: # no more retries + done = mock.MagicMock(uri=authzr.uri, times=authzr.times) + done.body.status = messages.STATUS_VALID + return done, [] + + # response (2nd result tuple element) is reduced to only + # Retry-After header contents represented as integer + # seconds; authzr.retries is a list of Retry-After + # headers, head(retries) is peeled of as a current + # Retry-After header, and tail(retries) is persisted for + # later poll() calls + return (mock.MagicMock(retries=authzr.retries[1:], + uri=authzr.uri + '.', times=authzr.times), + authzr.retries[0]) + self.net.poll = mock.MagicMock(side_effect=poll) + + mintime = 7 + + def retry_after(response, default): # pylint: disable=missing-docstring + # check that poll_and_request_issuance correctly passes mintime + self.assertEqual(default, mintime) + return clock.dt + datetime.timedelta(seconds=response) + self.net.retry_after = mock.MagicMock(side_effect=retry_after) + + def request_issuance(csr, authzrs): # pylint: disable=missing-docstring + return csr, authzrs + self.net.request_issuance = mock.MagicMock(side_effect=request_issuance) + + csr = mock.MagicMock() + authzrs = ( + mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)), + mock.MagicMock(uri='b', times=[], retries=(5,)), + ) + + cert, updated_authzrs = self.net.poll_and_request_issuance( + csr, authzrs, mintime=mintime) + self.assertTrue(cert[0] is csr) + self.assertTrue(cert[1] is updated_authzrs) + self.assertEqual(updated_authzrs[0].uri, 'a...') + self.assertEqual(updated_authzrs[1].uri, 'b.') + self.assertEqual(updated_authzrs[0].times, [ + datetime.datetime(2015, 3, 27), + # a is scheduled for 10, but b is polling [9..11), so it + # will be picked up as soon as b is finished, without + # additional sleeping + datetime.datetime(2015, 3, 27, 0, 0, 11), + datetime.datetime(2015, 3, 27, 0, 0, 33), + datetime.datetime(2015, 3, 27, 0, 1, 5), + ]) + self.assertEqual(updated_authzrs[1].times, [ + datetime.datetime(2015, 3, 27, 0, 0, 2), + datetime.datetime(2015, 3, 27, 0, 0, 9), + ]) + self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) + + def test_check_cert(self): + self.response.headers['Location'] = self.certr.uri + self.response.content = CERT.as_der() + self._mock_post_get() + self.assertEqual( + self.certr.update(body=CERT), self.net.check_cert(self.certr)) + + # TODO: split here and separate test + self.response.headers['Location'] = 'foo' + self.assertRaises( + errors.UnexpectedUpdate, self.net.check_cert, self.certr) + + def test_check_cert_missing_location(self): + self.response.content = CERT.as_der() + self._mock_post_get() + self.assertRaises(errors.ClientError, self.net.check_cert, self.certr) + + def test_refresh(self): + self.net.check_cert = mock.MagicMock() + self.assertEqual( + self.net.check_cert(self.certr), self.net.refresh(self.certr)) + + def test_fetch_chain(self): + # pylint: disable=protected-access + self.net._get_cert = mock.MagicMock() + self.net._get_cert.return_value = ("response", "certificate") + self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri)[1], + self.net.fetch_chain(self.certr)) + + def test_fetch_chain_no_up_link(self): + self.assertTrue(self.net.fetch_chain(self.certr.update( + cert_chain_uri=None)) is None) + + def test_revoke(self): + self._mock_post_get() + self.net.revoke(self.certr, when=messages.Revocation.NOW) + self.post.assert_called_once_with(self.certr.uri, mock.ANY) + + def test_revoke_bad_status_raises_error(self): + self.response.status_code = httplib.METHOD_NOT_ALLOWED + self._mock_post_get() + self.assertRaises(errors.ClientError, self.net.revoke, self.certr) + + +if __name__ == '__main__': + unittest.main() # pragma: no cover diff --git a/acme/errors.py b/acme/errors.py index 957e781af..5046d7aee 100644 --- a/acme/errors.py +++ b/acme/errors.py @@ -1,8 +1,15 @@ """ACME errors.""" from acme.jose import errors as jose_errors + class Error(Exception): """Generic ACME error.""" class SchemaValidationError(jose_errors.DeserializationError): """JSON schema ACME object validation error.""" + +class ClientError(Error): + """Network error.""" + +class UnexpectedUpdate(ClientError): + """Unexpected update.""" diff --git a/acme/jose/testdata/README b/acme/jose/testdata/README index 72ec557e0..be3d8b2f7 100644 --- a/acme/jose/testdata/README +++ b/acme/jose/testdata/README @@ -4,7 +4,8 @@ The following command has been used to generate test keys: and for the CSR: - python -c from 'letsencrypt.crypto_util import make_csr; - import pkg_resources; open("csr2.pem", - "w").write(make_csr(pkg_resources.resource_string("letsencrypt.tests", - "testdata/rsa512_key.pem"), ["example2.com"])[0])' + openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -outform DER > csr.der + +and for the certificate: + + openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der diff --git a/acme/jose/testdata/cert.der b/acme/jose/testdata/cert.der new file mode 100644 index 0000000000000000000000000000000000000000..5f1018505d81a50ed3239d829533deac5fcc2085 GIT binary patch literal 377 zcmXqLVk|XiVw7LN%*4pV#L2Ms(6oH-+lDa)ylk9WZ60mkc^MhGSs4t(3`Got*qB3E zn0dHUD-v@Ha#Hn@^K%X4#CZ)(4a|&;3``6RjLoCKTyr=Vr#=)57+D#Zy%`KVm>e0_ zlooFZd@FxGb01z;s66b16iPJW%*ddSVYv$pLpwieafaMs>~58{VWGc zu1@bVkOxUCvq%_-HDFi315zN&!fL?G$oL;EIG7z7c)I@!HO%vwut#mfG=7{>z}O6i?Kd^cJYN9>LqE5%h;CwZnWA40OQJj AMgRZ+ literal 0 HcmV?d00001 diff --git a/acme/jose/testdata/csr.der b/acme/jose/testdata/csr.der new file mode 100644 index 0000000000000000000000000000000000000000..adc29ff18463752b4b9ab26a0dd77d2621363725 GIT binary patch literal 210 zcmXqLJa16V#K>SEW+-AH#Ks)T!py^+T9KGrkdvyHoS$nDW5CPCsnzDu_MMlJk&%^w z*_*+@gUOL$O=u*A)ag|G9rW`hrfWw8)N9Mizmm^318OckeHsZ?f+1zL%^m z_ua)BZ+3d0>&un-HPz+C`j!&^w}+lGF*7nSE?_`5of~MnBZF(de`9^8<1$WWX~ABh zzuPx+%v!0Zd&ubDYwJ2|v1jw;{~Wg5`)^L5{K`Q6q!i`vv$n1I^5AE`uuA%MLEjl{ F*8oY6Puc(g literal 0 HcmV?d00001 diff --git a/acme/jose/testdata/csr2.pem b/acme/jose/testdata/csr2.pem deleted file mode 100644 index bd059a448..000000000 --- a/acme/jose/testdata/csr2.pem +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBXzCCAQkCAQAwejELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw -EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy -c2l0eSBvZiBNaWNoaWdhbjEVMBMGA1UEAwwMZXhhbXBsZTIuY29tMFwwDQYJKoZI -hvcNAQEBBQADSwAwSAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNH -tPXJmLlM8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAaAqMCgGCSqGSIb3 -DQEJDjEbMBkwFwYDVR0RBBAwDoIMZXhhbXBsZTIuY29tMA0GCSqGSIb3DQEBCwUA -A0EAwsdL4FLIgISKV4vXFmc6QTV7CjBiP4XmPFbeN+gMFdR7QcnRyyxSpXxB0v8Z -oqYboP5LGFt9zC6/9GyjcI9/IQ== ------END CERTIFICATE REQUEST----- diff --git a/docs/pkgs/acme/index.rst b/docs/pkgs/acme/index.rst index ea0743b1e..2df2615a5 100644 --- a/docs/pkgs/acme/index.rst +++ b/docs/pkgs/acme/index.rst @@ -7,6 +7,13 @@ :members: +Client +------ + +.. automodule:: acme.client + :members: + + Messages -------- diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index f5d9f5f44..d9078dbf2 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -5,14 +5,6 @@ class LetsEncryptClientError(Exception): """Generic Let's Encrypt client error.""" -class NetworkError(LetsEncryptClientError): - """Network error.""" - - -class UnexpectedUpdate(NetworkError): - """Unexpected update.""" - - class LetsEncryptReverterError(LetsEncryptClientError): """Let's Encrypt Reverter error.""" diff --git a/letsencrypt/network.py b/letsencrypt/network.py index 6d3be1afc..0f4d9d29b 100644 --- a/letsencrypt/network.py +++ b/letsencrypt/network.py @@ -1,230 +1,15 @@ -"""Networking for ACME protocol v02.""" -import datetime -import heapq -import httplib -import logging -import time - -import M2Crypto -import requests -import werkzeug - -from acme import jose -from acme import jws as acme_jws -from acme import messages - -from letsencrypt import errors +"""Networking for ACME protocol.""" +from acme import client -# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning -requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() - - -class Network(object): - """ACME networking. - - .. todo:: - Clean up raised error types hierarchy, document, and handle (wrap) - instances of `.DeserializationError` raised in `from_json()`. - - :ivar str new_reg_uri: Location of new-reg - :ivar key: `.JWK` (private) - :ivar alg: `.JWASignature` - :ivar bool verify_ssl: Verify SSL certificates? - - """ - - # TODO: Move below to acme module? - DER_CONTENT_TYPE = 'application/pkix-cert' - JSON_CONTENT_TYPE = 'application/json' - JSON_ERROR_CONTENT_TYPE = 'application/problem+json' - REPLAY_NONCE_HEADER = 'Replay-Nonce' - - def __init__(self, new_reg_uri, key, alg=jose.RS256, verify_ssl=True): - self.new_reg_uri = new_reg_uri - self.key = key - self.alg = alg - self.verify_ssl = verify_ssl - self._nonces = set() - - def _wrap_in_jws(self, obj, nonce): - """Wrap `JSONDeSerializable` object in JWS. - - .. todo:: Implement ``acmePath``. - - :param JSONDeSerializable obj: - :rtype: `.JWS` - - """ - dumps = obj.json_dumps() - logging.debug('Serialized JSON: %s', dumps) - return acme_jws.JWS.sign( - payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() - - @classmethod - def _check_response(cls, response, content_type=None): - """Check response content and its type. - - .. note:: - Checking is not strict: wrong server response ``Content-Type`` - HTTP header is ignored if response is an expected JSON object - (c.f. Boulder #56). - - :param str content_type: Expected Content-Type response header. - If JSON is expected and not present in server response, this - function will raise an error. Otherwise, wrong Content-Type - is ignored, but logged. - - :raises letsencrypt.messages.Error: If server response body - carries HTTP Problem (draft-ietf-appsawg-http-problem-00). - :raises letsencrypt.errors.NetworkError: In case of other - networking errors. - - """ - response_ct = response.headers.get('Content-Type') - try: - # TODO: response.json() is called twice, once here, and - # once in _get and _post clients - jobj = response.json() - except ValueError as error: - jobj = None - - if not response.ok: - if jobj is not None: - if response_ct != cls.JSON_ERROR_CONTENT_TYPE: - logging.debug( - 'Ignoring wrong Content-Type (%r) for JSON Error', - response_ct) - try: - logging.error("Error: %s", jobj) - logging.error("Response from server: %s", response.content) - raise messages.Error.from_json(jobj) - except jose.DeserializationError as error: - # Couldn't deserialize JSON object - raise errors.NetworkError((response, error)) - else: - # response is not JSON object - raise errors.NetworkError(response) - else: - if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: - logging.debug( - 'Ignoring wrong Content-Type (%r) for JSON decodable ' - 'response', response_ct) - - if content_type == cls.JSON_CONTENT_TYPE and jobj is None: - raise errors.NetworkError( - 'Unexpected response Content-Type: {0}'.format(response_ct)) - - def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs): - """Send GET request. - - :raises letsencrypt.errors.NetworkError: - - :returns: HTTP Response - :rtype: `requests.Response` - - """ - logging.debug('Sending GET request to %s', uri) - kwargs.setdefault('verify', self.verify_ssl) - try: - response = requests.get(uri, **kwargs) - except requests.exceptions.RequestException as error: - raise errors.NetworkError(error) - self._check_response(response, content_type=content_type) - return response - - def _add_nonce(self, response): - if self.REPLAY_NONCE_HEADER in response.headers: - nonce = response.headers[self.REPLAY_NONCE_HEADER] - error = acme_jws.Header.validate_nonce(nonce) - if error is None: - logging.debug('Storing nonce: %r', nonce) - self._nonces.add(nonce) - else: - raise errors.NetworkError('Invalid nonce ({0}): {1}'.format( - nonce, error)) - else: - raise errors.NetworkError( - 'Server {0} response did not include a replay nonce'.format( - response.request.method)) - - def _get_nonce(self, uri): - if not self._nonces: - logging.debug('Requesting fresh nonce by sending HEAD to %s', uri) - self._add_nonce(requests.head(uri)) - return self._nonces.pop() - - def _post(self, uri, obj, content_type=JSON_CONTENT_TYPE, **kwargs): - """Send POST data. - - :param JSONDeSerializable obj: Will be wrapped in JWS. - :param str content_type: Expected ``Content-Type``, fails if not set. - - :raises acme.messages.NetworkError: - - :returns: HTTP Response - :rtype: `requests.Response` - - """ - data = self._wrap_in_jws(obj, self._get_nonce(uri)) - logging.debug('Sending POST data to %s: %s', uri, data) - kwargs.setdefault('verify', self.verify_ssl) - try: - response = requests.post(uri, data=data, **kwargs) - except requests.exceptions.RequestException as error: - raise errors.NetworkError(error) - logging.debug('Received response %s: %r', response, response.text) - - self._add_nonce(response) - self._check_response(response, content_type=content_type) - return response - - @classmethod - def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, - terms_of_service=None): - terms_of_service = ( - response.links['terms-of-service']['url'] - if 'terms-of-service' in response.links else terms_of_service) - - if new_authzr_uri is None: - try: - new_authzr_uri = response.links['next']['url'] - except KeyError: - raise errors.NetworkError('"next" link missing') - - return messages.RegistrationResource( - body=messages.Registration.from_json(response.json()), - uri=response.headers.get('Location', uri), - new_authzr_uri=new_authzr_uri, - terms_of_service=terms_of_service) - - def register(self, contact=messages.Registration._fields[ - 'contact'].default): - """Register. - - :param contact: Contact list, as accepted by `.Registration` - :type contact: `tuple` - - :returns: Registration Resource. - :rtype: `.RegistrationResource` - - :raises letsencrypt.errors.UnexpectedUpdate: - - """ - new_reg = messages.Registration(contact=contact) - - response = self._post(self.new_reg_uri, new_reg) - assert response.status_code == httplib.CREATED # TODO: handle errors - - regr = self._regr_from_response(response) - if regr.body.key != self.key.public() or regr.body.contact != contact: - raise errors.UnexpectedUpdate(regr) - - return regr +class Network(client.Client): + """ACME networking.""" def register_from_account(self, account): """Register with server. + .. todo:: this should probably not be a part of network... + :param account: Account :type account: :class:`letsencrypt.account.Account` @@ -239,344 +24,3 @@ class Network(object): account.regr = self.register(contact=tuple( det for det in details if det is not None)) return account - - def update_registration(self, regr): - """Update registration. - - :pram regr: Registration Resource. - :type regr: `.RegistrationResource` - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - response = self._post(regr.uri, regr.body) - - # TODO: Boulder returns httplib.ACCEPTED - #assert response.status_code == httplib.OK - - # TODO: Boulder does not set Location or Link on update - # (c.f. acme-spec #94) - updated_regr = self._regr_from_response( - response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, - terms_of_service=regr.terms_of_service) - if updated_regr != regr: - raise errors.UnexpectedUpdate(regr) - - return updated_regr - - def agree_to_tos(self, regr): - """Agree to the terms-of-service. - - Agree to the terms-of-service in a Registration Resource. - - :param regr: Registration Resource. - :type regr: `.RegistrationResource` - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - return self.update_registration( - regr.update(body=regr.body.update(agreement=regr.terms_of_service))) - - def _authzr_from_response(self, response, identifier, - uri=None, new_cert_uri=None): - # pylint: disable=no-self-use - if new_cert_uri is None: - try: - new_cert_uri = response.links['next']['url'] - except KeyError: - raise errors.NetworkError('"next" link missing') - - authzr = messages.AuthorizationResource( - body=messages.Authorization.from_json(response.json()), - uri=response.headers.get('Location', uri), - new_cert_uri=new_cert_uri) - if authzr.body.identifier != identifier: - raise errors.UnexpectedUpdate(authzr) - return authzr - - def request_challenges(self, identifier, new_authzr_uri): - """Request challenges. - - :param identifier: Identifier to be challenged. - :type identifier: `.messages.Identifier` - - :param str new_authzr_uri: new-authorization URI - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - """ - new_authz = messages.Authorization(identifier=identifier) - response = self._post(new_authzr_uri, new_authz) - assert response.status_code == httplib.CREATED # TODO: handle errors - return self._authzr_from_response(response, identifier) - - def request_domain_challenges(self, domain, new_authz_uri): - """Request challenges for domain names. - - This is simply a convenience function that wraps around - `request_challenges`, but works with domain names instead of - generic identifiers. - - :param str domain: Domain name to be challenged. - :param str new_authzr_uri: new-authorization URI - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - """ - return self.request_challenges(messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri) - - def answer_challenge(self, challb, response): - """Answer challenge. - - :param challb: Challenge Resource body. - :type challb: `.ChallengeBody` - - :param response: Corresponding Challenge response - :type response: `.challenges.ChallengeResponse` - - :returns: Challenge Resource with updated body. - :rtype: `.ChallengeResource` - - :raises errors.UnexpectedUpdate: - - """ - response = self._post(challb.uri, response) - try: - authzr_uri = response.links['up']['url'] - except KeyError: - raise errors.NetworkError('"up" Link header missing') - challr = messages.ChallengeResource( - authzr_uri=authzr_uri, - body=messages.ChallengeBody.from_json(response.json())) - # TODO: check that challr.uri == response.headers['Location']? - if challr.uri != challb.uri: - raise errors.UnexpectedUpdate(challr.uri) - return challr - - @classmethod - def retry_after(cls, response, default): - """Compute next `poll` time based on response ``Retry-After`` header. - - :param response: Response from `poll`. - :type response: `requests.Response` - - :param int default: Default value (in seconds), used when - ``Retry-After`` header is not present or invalid. - - :returns: Time point when next `poll` should be performed. - :rtype: `datetime.datetime` - - """ - retry_after = response.headers.get('Retry-After', str(default)) - try: - seconds = int(retry_after) - except ValueError: - # pylint: disable=no-member - decoded = werkzeug.parse_date(retry_after) # RFC1123 - if decoded is None: - seconds = default - else: - return decoded - - return datetime.datetime.now() + datetime.timedelta(seconds=seconds) - - def poll(self, authzr): - """Poll Authorization Resource for status. - - :param authzr: Authorization Resource - :type authzr: `.AuthorizationResource` - - :returns: Updated Authorization Resource and HTTP response. - - :rtype: (`.AuthorizationResource`, `requests.Response`) - - """ - response = self._get(authzr.uri) - updated_authzr = self._authzr_from_response( - response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) - # TODO: check and raise UnexpectedUpdate - return updated_authzr, response - - def request_issuance(self, csr, authzrs): - """Request issuance. - - :param csr: CSR - :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` - - :param authzrs: `list` of `.AuthorizationResource` - - :returns: Issued certificate - :rtype: `.messages.CertificateResource` - - """ - assert authzrs, "Authorizations list is empty" - logging.debug("Requesting issuance...") - - # TODO: assert len(authzrs) == number of SANs - req = messages.CertificateRequest( - csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) - - content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument - response = self._post( - authzrs[0].new_cert_uri, # TODO: acme-spec #90 - req, - content_type=content_type, - headers={'Accept': content_type}) - - cert_chain_uri = response.links.get('up', {}).get('url') - - try: - uri = response.headers['Location'] - except KeyError: - raise errors.NetworkError('"Location" Header missing') - - return messages.CertificateResource( - uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, - body=jose.ComparableX509( - M2Crypto.X509.load_cert_der_string(response.content))) - - def poll_and_request_issuance(self, csr, authzrs, mintime=5): - """Poll and request issuance. - - This function polls all provided Authorization Resource URIs - until all challenges are valid, respecting ``Retry-After`` HTTP - headers, and then calls `request_issuance`. - - .. todo:: add `max_attempts` or `timeout` - - :param csr: CSR. - :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` - - :param authzrs: `list` of `.AuthorizationResource` - - :param int mintime: Minimum time before next attempt, used if - ``Retry-After`` is not present in the response. - - :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is - the issued certificate (`.messages.CertificateResource.), - and ``updated_authzrs`` is a `tuple` consisting of updated - Authorization Resources (`.AuthorizationResource`) as - present in the responses from server, and in the same order - as the input ``authzrs``. - :rtype: `tuple` - - """ - # priority queue with datetime (based on Retry-After) as key, - # and original Authorization Resource as value - waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs] - # mapping between original Authorization Resource and the most - # recently updated one - updated = dict((authzr, authzr) for authzr in authzrs) - - while waiting: - # find the smallest Retry-After, and sleep if necessary - when, authzr = heapq.heappop(waiting) - now = datetime.datetime.now() - if when > now: - seconds = (when - now).seconds - logging.debug('Sleeping for %d seconds', seconds) - time.sleep(seconds) - - # Note that we poll with the latest updated Authorization - # URI, which might have a different URI than initial one - updated_authzr, response = self.poll(updated[authzr]) - updated[authzr] = updated_authzr - - if updated_authzr.body.status != messages.STATUS_VALID: - # push back to the priority queue, with updated retry_after - heapq.heappush(waiting, (self.retry_after( - response, default=mintime), authzr)) - - updated_authzrs = tuple(updated[authzr] for authzr in authzrs) - return self.request_issuance(csr, updated_authzrs), updated_authzrs - - def _get_cert(self, uri): - """Returns certificate from URI. - - :param str uri: URI of certificate - - :returns: tuple of the form - (response, :class:`acme.jose.ComparableX509`) - :rtype: tuple - - """ - content_type = self.DER_CONTENT_TYPE # TODO: make it a param - response = self._get(uri, headers={'Accept': content_type}, - content_type=content_type) - return response, jose.ComparableX509( - M2Crypto.X509.load_cert_der_string(response.content)) - - def check_cert(self, certr): - """Check for new cert. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Updated Certificate Resource. - :rtype: `.CertificateResource` - - """ - # TODO: acme-spec 5.1 table action should be renamed to - # "refresh cert", and this method integrated with self.refresh - response, cert = self._get_cert(certr.uri) - if 'Location' not in response.headers: - raise errors.NetworkError('Location header missing') - if response.headers['Location'] != certr.uri: - raise errors.UnexpectedUpdate(response.text) - return certr.update(body=cert) - - def refresh(self, certr): - """Refresh certificate. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Updated Certificate Resource. - :rtype: `.CertificateResource` - - """ - # TODO: If a client sends a refresh request and the server is - # not willing to refresh the certificate, the server MUST - # respond with status code 403 (Forbidden) - return self.check_cert(certr) - - def fetch_chain(self, certr): - """Fetch chain for certificate. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Certificate chain, or `None` if no "up" Link was provided. - :rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509` - - """ - if certr.cert_chain_uri is not None: - return self._get_cert(certr.cert_chain_uri)[1] - else: - return None - - def revoke(self, certr, when=messages.Revocation.NOW): - """Revoke certificate. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :param when: When should the revocation take place? Takes - the same values as `.messages.Revocation.revoke`. - - :raises letsencrypt.errors.NetworkError: If revocation is - unsuccessful. - - """ - rev = messages.Revocation(revoke=when, authorizations=tuple( - authzr.uri for authzr in certr.authzrs)) - response = self._post(certr.uri, rev) - if response.status_code != httplib.OK: - raise errors.NetworkError( - 'Successful revocation must return HTTP OK status') diff --git a/letsencrypt/tests/network_test.py b/letsencrypt/tests/network_test.py index 586dc7ecb..6acb11315 100644 --- a/letsencrypt/tests/network_test.py +++ b/letsencrypt/tests/network_test.py @@ -1,281 +1,27 @@ """Tests for letsencrypt.network.""" -import datetime -import httplib -import os -import pkg_resources import shutil import tempfile import unittest -import M2Crypto import mock -import requests - -from acme import challenges -from acme import jose -from acme import jws as acme_jws -from acme import messages from letsencrypt import account -from letsencrypt import errors - - -CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'cert.pem')))) -CERT2 = jose.ComparableX509(M2Crypto.X509.load_cert_string( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'cert-san.pem')))) -CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'csr.pem')))) -KEY = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) -KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa256_key.pem'))) class NetworkTest(unittest.TestCase): """Tests for letsencrypt.network.Network.""" - # pylint: disable=too-many-instance-attributes,too-many-public-methods - def setUp(self): - self.verify_ssl = mock.MagicMock() - self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) - from letsencrypt.network import Network self.net = Network( - new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', - key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) - self.nonce = jose.b64encode('Nonce') - self.net._nonces.add(self.nonce) # pylint: disable=protected-access - - self.response = mock.MagicMock(ok=True, status_code=httplib.OK) - self.response.headers = {} - self.response.links = {} - - self.post = mock.MagicMock(return_value=self.response) - self.get = mock.MagicMock(return_value=self.response) - - self.identifier = messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value='example.com') + new_reg_uri=None, key=None, alg=None, verify_ssl=None) self.config = mock.Mock(accounts_dir=tempfile.mkdtemp()) - - # Registration self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') - reg = messages.Registration( - contact=self.contact, key=KEY.public(), recovery_token='t') - self.regr = messages.RegistrationResource( - body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', - new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', - terms_of_service='https://www.letsencrypt-demo.org/tos') - - # Authorization - authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' - challb = messages.ChallengeBody( - uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, - chall=challenges.DNS(token='foo')) - self.challr = messages.ChallengeResource( - body=challb, authzr_uri=authzr_uri) - self.authz = messages.Authorization( - identifier=messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value='example.com'), - challenges=(challb,), combinations=None) - self.authzr = messages.AuthorizationResource( - body=self.authz, uri=authzr_uri, - new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') - - # Request issuance - self.certr = messages.CertificateResource( - body=CERT, authzrs=(self.authzr,), - uri='https://www.letsencrypt-demo.org/acme/cert/1', - cert_chain_uri='https://www.letsencrypt-demo.org/ca') def tearDown(self): shutil.rmtree(self.config.accounts_dir) - def _mock_post_get(self): - # pylint: disable=protected-access - self.net._post = self.post - self.net._get = self.get - - def test_init(self): - self.assertTrue(self.net.verify_ssl is self.verify_ssl) - - def test_wrap_in_jws(self): - class MockJSONDeSerializable(jose.JSONDeSerializable): - # pylint: disable=missing-docstring - def __init__(self, value): - self.value = value - def to_partial_json(self): - return self.value - @classmethod - def from_json(cls, value): - pass # pragma: no cover - # pylint: disable=protected-access - jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce='Tg') - jws = acme_jws.JWS.json_loads(jws_dump) - self.assertEqual(jws.payload, '"foo"') - self.assertEqual(jws.signature.combined.nonce, 'Tg') - # TODO: check that nonce is in protected header - - def test_check_response_not_ok_jobj_no_error(self): - self.response.ok = False - self.response.json.return_value = {} - # pylint: disable=protected-access - self.assertRaises( - errors.NetworkError, self.net._check_response, self.response) - - def test_check_response_not_ok_jobj_error(self): - self.response.ok = False - self.response.json.return_value = messages.Error( - detail='foo', typ='serverInternal', title='some title').to_json() - # pylint: disable=protected-access - self.assertRaises( - messages.Error, self.net._check_response, self.response) - - def test_check_response_not_ok_no_jobj(self): - self.response.ok = False - self.response.json.side_effect = ValueError - # pylint: disable=protected-access - self.assertRaises( - errors.NetworkError, self.net._check_response, self.response) - - def test_check_response_ok_no_jobj_ct_required(self): - self.response.json.side_effect = ValueError - for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: - self.response.headers['Content-Type'] = response_ct - # pylint: disable=protected-access - self.assertRaises( - errors.NetworkError, self.net._check_response, self.response, - content_type=self.net.JSON_CONTENT_TYPE) - - def test_check_response_ok_no_jobj_no_ct(self): - self.response.json.side_effect = ValueError - for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: - self.response.headers['Content-Type'] = response_ct - # pylint: disable=protected-access - self.net._check_response(self.response) - - def test_check_response_jobj(self): - self.response.json.return_value = {} - for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: - self.response.headers['Content-Type'] = response_ct - # pylint: disable=protected-access - self.net._check_response(self.response) - - @mock.patch('letsencrypt.network.requests') - def test_get_requests_error_passthrough(self, requests_mock): - requests_mock.exceptions = requests.exceptions - requests_mock.get.side_effect = requests.exceptions.RequestException - # pylint: disable=protected-access - self.assertRaises(errors.NetworkError, self.net._get, 'uri') - - @mock.patch('letsencrypt.network.requests') - def test_get(self, requests_mock): - # pylint: disable=protected-access - self.net._check_response = mock.MagicMock() - self.net._get('uri', content_type='ct') - self.net._check_response.assert_called_once_with( - requests_mock.get('uri'), content_type='ct') - - def _mock_wrap_in_jws(self): - # pylint: disable=protected-access - self.net._wrap_in_jws = self.wrap_in_jws - - @mock.patch('letsencrypt.network.requests') - def test_post_requests_error_passthrough(self, requests_mock): - requests_mock.exceptions = requests.exceptions - requests_mock.post.side_effect = requests.exceptions.RequestException - # pylint: disable=protected-access - self._mock_wrap_in_jws() - self.assertRaises( - errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - - @mock.patch('letsencrypt.network.requests') - def test_post(self, requests_mock): - # pylint: disable=protected-access - self.net._check_response = mock.MagicMock() - self._mock_wrap_in_jws() - requests_mock.post().headers = { - self.net.REPLAY_NONCE_HEADER: self.nonce} - self.net._post('uri', mock.sentinel.obj, content_type='ct') - self.net._check_response.assert_called_once_with( - requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') - - @mock.patch('letsencrypt.network.requests') - def test_post_replay_nonce_handling(self, requests_mock): - # pylint: disable=protected-access - self.net._check_response = mock.MagicMock() - self._mock_wrap_in_jws() - - self.net._nonces.clear() - self.assertRaises( - errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - - nonce2 = jose.b64encode('Nonce2') - requests_mock.head('uri').headers = { - self.net.REPLAY_NONCE_HEADER: nonce2} - requests_mock.post('uri').headers = { - self.net.REPLAY_NONCE_HEADER: self.nonce} - - self.net._post('uri', mock.sentinel.obj) - - requests_mock.head.assert_called_with('uri') - self.wrap_in_jws.assert_called_once_with(mock.sentinel.obj, nonce2) - self.assertEqual(self.net._nonces, set([self.nonce])) - - # wrong nonce - requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'} - self.assertRaises( - errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - - @mock.patch('letsencrypt.client.network.requests') - def test_get_post_verify_ssl(self, requests_mock): - # pylint: disable=protected-access - self._mock_wrap_in_jws() - self.net._check_response = mock.MagicMock() - - for verify_ssl in [True, False]: - self.net.verify_ssl = verify_ssl - self.net._get('uri') - self.net._nonces.add('N') - requests_mock.post().headers = { - self.net.REPLAY_NONCE_HEADER: self.nonce} - self.net._post('uri', mock.sentinel.obj) - requests_mock.get.assert_called_once_with('uri', verify=verify_ssl) - requests_mock.post.assert_called_with( - 'uri', data=mock.sentinel.wrapped, verify=verify_ssl) - requests_mock.reset_mock() - - def test_register(self): - self.response.status_code = httplib.CREATED - self.response.json.return_value = self.regr.body.to_json() - self.response.headers['Location'] = self.regr.uri - self.response.links.update({ - 'next': {'url': self.regr.new_authzr_uri}, - 'terms-of-service': {'url': self.regr.terms_of_service}, - }) - - self._mock_post_get() - self.assertEqual(self.regr, self.net.register(self.contact)) - # TODO: test POST call arguments - - # TODO: split here and separate test - reg_wrong_key = self.regr.body.update(key=KEY2.public()) - self.response.json.return_value = reg_wrong_key.to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.net.register, self.contact) - - def test_register_missing_next(self): - self.response.status_code = httplib.CREATED - self._mock_post_get() - self.assertRaises( - errors.NetworkError, self.net.register, self.regr.body) - def test_register_from_account(self): self.net.register = mock.Mock() acc = account.Account( @@ -299,265 +45,6 @@ class NetworkTest(unittest.TestCase): self.net.register_from_account(acc2) self.net.register.assert_called_with(contact=()) - def test_update_registration(self): - self.response.headers['Location'] = self.regr.uri - self.response.json.return_value = self.regr.body.to_json() - self._mock_post_get() - self.assertEqual(self.regr, self.net.update_registration(self.regr)) - - # TODO: split here and separate test - self.response.json.return_value = self.regr.body.update( - contact=()).to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.net.update_registration, self.regr) - - def test_agree_to_tos(self): - self.net.update_registration = mock.Mock() - self.net.agree_to_tos(self.regr) - regr = self.net.update_registration.call_args[0][0] - self.assertEqual(self.regr.terms_of_service, regr.body.agreement) - - def test_request_challenges(self): - self.response.status_code = httplib.CREATED - self.response.headers['Location'] = self.authzr.uri - self.response.json.return_value = self.authz.to_json() - self.response.links = { - 'next': {'url': self.authzr.new_cert_uri}, - } - - self._mock_post_get() - self.net.request_challenges(self.identifier, self.authzr.uri) - # TODO: test POST call arguments - - # TODO: split here and separate test - self.response.json.return_value = self.authz.update( - identifier=self.identifier.update(value='foo')).to_json() - self.assertRaises(errors.UnexpectedUpdate, self.net.request_challenges, - self.identifier, self.authzr.uri) - - def test_request_challenges_missing_next(self): - self.response.status_code = httplib.CREATED - self._mock_post_get() - self.assertRaises( - errors.NetworkError, self.net.request_challenges, - self.identifier, self.regr) - - def test_request_domain_challenges(self): - self.net.request_challenges = mock.MagicMock() - self.assertEqual( - self.net.request_challenges(self.identifier), - self.net.request_domain_challenges('example.com', self.regr)) - - def test_answer_challenge(self): - self.response.links['up'] = {'url': self.challr.authzr_uri} - self.response.json.return_value = self.challr.body.to_json() - - chall_response = challenges.DNSResponse() - - self._mock_post_get() - self.net.answer_challenge(self.challr.body, chall_response) - - # TODO: split here and separate test - self.assertRaises(errors.UnexpectedUpdate, self.net.answer_challenge, - self.challr.body.update(uri='foo'), chall_response) - - def test_answer_challenge_missing_next(self): - self._mock_post_get() - self.assertRaises(errors.NetworkError, self.net.answer_challenge, - self.challr.body, challenges.DNSResponse()) - - def test_retry_after_date(self): - self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' - self.assertEqual( - datetime.datetime(1999, 12, 31, 23, 59, 59), - self.net.retry_after(response=self.response, default=10)) - - @mock.patch('letsencrypt.network.datetime') - def test_retry_after_invalid(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.response.headers['Retry-After'] = 'foooo' - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.net.retry_after(response=self.response, default=10)) - - @mock.patch('letsencrypt.network.datetime') - def test_retry_after_seconds(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.response.headers['Retry-After'] = '50' - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 50), - self.net.retry_after(response=self.response, default=10)) - - @mock.patch('letsencrypt.network.datetime') - def test_retry_after_missing(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.net.retry_after(response=self.response, default=10)) - - def test_poll(self): - self.response.json.return_value = self.authzr.body.to_json() - self._mock_post_get() - self.assertEqual((self.authzr, self.response), - self.net.poll(self.authzr)) - - # TODO: split here and separate test - self.response.json.return_value = self.authz.update( - identifier=self.identifier.update(value='foo')).to_json() - self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) - - def test_request_issuance(self): - self.response.content = CERT.as_der() - self.response.headers['Location'] = self.certr.uri - self.response.links['up'] = {'url': self.certr.cert_chain_uri} - self._mock_post_get() - self.assertEqual( - self.certr, self.net.request_issuance(CSR, (self.authzr,))) - # TODO: check POST args - - def test_request_issuance_missing_up(self): - self.response.content = CERT.as_der() - self.response.headers['Location'] = self.certr.uri - self._mock_post_get() - self.assertEqual( - self.certr.update(cert_chain_uri=None), - self.net.request_issuance(CSR, (self.authzr,))) - - def test_request_issuance_missing_location(self): - self._mock_post_get() - self.assertRaises( - errors.NetworkError, self.net.request_issuance, - CSR, (self.authzr,)) - - @mock.patch('letsencrypt.network.datetime') - @mock.patch('letsencrypt.network.time') - def test_poll_and_request_issuance(self, time_mock, dt_mock): - # clock.dt | pylint: disable=no-member - clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) - - def sleep(seconds): - """increment clock""" - clock.dt += datetime.timedelta(seconds=seconds) - time_mock.sleep.side_effect = sleep - - def now(): - """return current clock value""" - return clock.dt - dt_mock.datetime.now.side_effect = now - dt_mock.timedelta = datetime.timedelta - - def poll(authzr): # pylint: disable=missing-docstring - # record poll start time based on the current clock value - authzr.times.append(clock.dt) - - # suppose it takes 2 seconds for server to produce the - # result, increment clock - clock.dt += datetime.timedelta(seconds=2) - - if not authzr.retries: # no more retries - done = mock.MagicMock(uri=authzr.uri, times=authzr.times) - done.body.status = messages.STATUS_VALID - return done, [] - - # response (2nd result tuple element) is reduced to only - # Retry-After header contents represented as integer - # seconds; authzr.retries is a list of Retry-After - # headers, head(retries) is peeled of as a current - # Retry-After header, and tail(retries) is persisted for - # later poll() calls - return (mock.MagicMock(retries=authzr.retries[1:], - uri=authzr.uri + '.', times=authzr.times), - authzr.retries[0]) - self.net.poll = mock.MagicMock(side_effect=poll) - - mintime = 7 - - def retry_after(response, default): # pylint: disable=missing-docstring - # check that poll_and_request_issuance correctly passes mintime - self.assertEqual(default, mintime) - return clock.dt + datetime.timedelta(seconds=response) - self.net.retry_after = mock.MagicMock(side_effect=retry_after) - - def request_issuance(csr, authzrs): # pylint: disable=missing-docstring - return csr, authzrs - self.net.request_issuance = mock.MagicMock(side_effect=request_issuance) - - csr = mock.MagicMock() - authzrs = ( - mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)), - mock.MagicMock(uri='b', times=[], retries=(5,)), - ) - - cert, updated_authzrs = self.net.poll_and_request_issuance( - csr, authzrs, mintime=mintime) - self.assertTrue(cert[0] is csr) - self.assertTrue(cert[1] is updated_authzrs) - self.assertEqual(updated_authzrs[0].uri, 'a...') - self.assertEqual(updated_authzrs[1].uri, 'b.') - self.assertEqual(updated_authzrs[0].times, [ - datetime.datetime(2015, 3, 27), - # a is scheduled for 10, but b is polling [9..11), so it - # will be picked up as soon as b is finished, without - # additional sleeping - datetime.datetime(2015, 3, 27, 0, 0, 11), - datetime.datetime(2015, 3, 27, 0, 0, 33), - datetime.datetime(2015, 3, 27, 0, 1, 5), - ]) - self.assertEqual(updated_authzrs[1].times, [ - datetime.datetime(2015, 3, 27, 0, 0, 2), - datetime.datetime(2015, 3, 27, 0, 0, 9), - ]) - self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) - - def test_check_cert(self): - self.response.headers['Location'] = self.certr.uri - self.response.content = CERT2.as_der() - self._mock_post_get() - self.assertEqual( - self.certr.update(body=CERT2), self.net.check_cert(self.certr)) - - # TODO: split here and separate test - self.response.headers['Location'] = 'foo' - self.assertRaises( - errors.UnexpectedUpdate, self.net.check_cert, self.certr) - - def test_check_cert_missing_location(self): - self.response.content = CERT2.as_der() - self._mock_post_get() - self.assertRaises(errors.NetworkError, self.net.check_cert, self.certr) - - def test_refresh(self): - self.net.check_cert = mock.MagicMock() - self.assertEqual( - self.net.check_cert(self.certr), self.net.refresh(self.certr)) - - def test_fetch_chain(self): - # pylint: disable=protected-access - self.net._get_cert = mock.MagicMock() - self.net._get_cert.return_value = ("response", "certificate") - self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri)[1], - self.net.fetch_chain(self.certr)) - - def test_fetch_chain_no_up_link(self): - self.assertTrue(self.net.fetch_chain(self.certr.update( - cert_chain_uri=None)) is None) - - def test_revoke(self): - self._mock_post_get() - self.net.revoke(self.certr, when=messages.Revocation.NOW) - self.post.assert_called_once_with(self.certr.uri, mock.ANY) - - def test_revoke_bad_status_raises_error(self): - self.response.status_code = httplib.METHOD_NOT_ALLOWED - self._mock_post_get() - self.assertRaises(errors.NetworkError, self.net.revoke, self.certr) - if __name__ == '__main__': unittest.main() # pragma: no cover From fa0988289212f752c00fb57679eedf03e029b0d2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 22 Jun 2015 01:09:43 -0700 Subject: [PATCH 075/109] Change permission error message Commonly, this will be caused by a failure to sudo, so the previous text was not necessarily going to be helpful. --- letsencrypt/le_util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index ba2427c79..6bf26ff7e 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -33,8 +33,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): if exception.errno == errno.EEXIST: if not check_permissions(directory, mode, uid): raise errors.LetsEncryptClientError( - "%s exists, but does not have the proper " - "permissions or owner" % directory) + "%s exists, this client can't access it" % directory) else: raise From 90dae9fd880b7ff07ee3f1b76d862b8419eaf22a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 11:07:20 +0000 Subject: [PATCH 076/109] Update restified example script and rename to acme_client.py --- examples/acme_client.py | 45 +++++++++++++++++++++++++++++++++++++++++ examples/restified.py | 42 -------------------------------------- 2 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 examples/acme_client.py delete mode 100644 examples/restified.py diff --git a/examples/acme_client.py b/examples/acme_client.py new file mode 100644 index 000000000..09ff2bfc3 --- /dev/null +++ b/examples/acme_client.py @@ -0,0 +1,45 @@ +"""Example script showing how to use acme client API.""" +import logging +import os +import pkg_resources + +import Crypto.PublicKey.RSA +import M2Crypto + +from acme import client +from acme import messages +from acme import jose + + +logging.basicConfig(level=logging.DEBUG) + + +NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg' +BITS = 2048 # minimum for Boulder +DOMAIN = 'example1.com' # example.com is ignored by Boulder + +key = jose.JWKRSA.load( + Crypto.PublicKey.RSA.generate(BITS).exportKey(format="PEM")) +acme = client.Client(NEW_REG_URL, key) + +regr = acme.register(contact=()) +logging.info('Auto-accepting TOS: %s', regr.terms_of_service) +acme.update_registration(regr.update( + body=regr.body.update(agreement=regr.terms_of_service))) +logging.debug(regr) + +authzr = acme.request_challenges( + identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN), + new_authzr_uri=regr.new_authzr_uri) +logging.debug(authzr) + +authzr, authzr_response = acme.poll(authzr) + +csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'csr.der')), + M2Crypto.X509.FORMAT_DER) +try: + acme.request_issuance(csr, (authzr,)) +except messages.Error as error: + print ("This script is doomed to fail as no authorization " + "challenges are ever solved. Error from server: {0}".format(error)) diff --git a/examples/restified.py b/examples/restified.py deleted file mode 100644 index cfd7fa8dd..000000000 --- a/examples/restified.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -import os -import pkg_resources - -import M2Crypto - -from acme import messages -from acme import jose - -from letsencrypt import network - - -logger = logging.getLogger() -logger.setLevel(logging.DEBUG) - -NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg' - -key = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) -net = network.Network(NEW_REG_URL, key) - -regr = net.register(contact=( - 'mailto:cert-admin@example.com', 'tel:+12025551212')) -logging.info('Auto-accepting TOS: %s', regr.terms_of_service) -net.update_registration(regr.update( - body=regr.body.update(agreement=regr.terms_of_service))) -logging.debug(regr) - -authzr = net.request_challenges( - identifier=messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value='example1.com'), - new_authzr_uri=regr.new_authzr_uri) -logging.debug(authzr) - -authzr, authzr_response = net.poll(authzr) - -csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'csr.pem'))) -try: - net.request_issuance(csr, (authzr,)) -except messages.Error as error: - print error.detail From f408ac7296782789cb802863e5cad69804d67eb0 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 22 Jun 2015 09:37:57 -0700 Subject: [PATCH 077/109] Draft basic usage text --- letsencrypt/cli.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index a48336fe7..36055d909 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -28,6 +28,38 @@ from letsencrypt.display import ops as display_ops from letsencrypt.plugins import disco as plugins_disco +USAGE = """ + letsencrypt [SUBCOMMAND] [options] [domains] + +The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates. By +default, it will attempt to use a webserver both for obtaining and installing +the cert. Major SUBCOMMANDS are: + + (default) Obtain & install a cert in your current webserver + auth Authenticate & obtain cert, but do not install it + install Install a previously obtained cert in a server + revoke Revoke a previously obtained certificate + rollback Rollback server configuration changes made during install + config-changes Show changes made to server config during installation + +Choice of server for authentication/installation: + + --apache Use the Apache plugin for authentication & installation + --nginx Use the Nginx plugin for authentication & installation + --standalone Run a standalone HTTPS server (for authentication only) + OR: + --authenticator standalone --installer nginx + +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 + sucommands +""" + + def _account_init(args, config): le_util.make_or_verify_dir( From cf76593fa70f4fb5743946feab5df939a23ef153 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 18:03:04 +0000 Subject: [PATCH 078/109] Remove constants.NETSTAT. Update docs for IConfig.server. --- letsencrypt/constants.py | 4 ---- letsencrypt/interfaces.py | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 47539615d..356b8ed14 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -81,7 +81,3 @@ ACCOUNT_KEYS_DIR = "keys" REC_TOKEN_DIR = "recovery_tokens" """Directory where all recovery tokens are saved (relative to IConfig.work_dir).""" - -NETSTAT = "/bin/netstat" -"""Location of netstat binary for checking whether a listener is already -running on the specified port (Linux-specific).""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index c0d44a134..d2a420d00 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -148,8 +148,7 @@ class IConfig(zope.interface.Interface): """ server = zope.interface.Attribute( - "CA hostname (and optionally :port). The server certificate must " - "be trusted in order to avoid further modifications to the client.") + "ACME new registration URI (including /acme/new-reg).") email = zope.interface.Attribute( "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") From 1720864b443351c473a130b9a6897f0b65cb18c2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 19:55:47 +0000 Subject: [PATCH 079/109] acme.client: locally disable too-many-instance-attributes. --- acme/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/client.py b/acme/client.py index c0eda0fa3..629048d03 100644 --- a/acme/client.py +++ b/acme/client.py @@ -19,7 +19,7 @@ from acme import messages requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() -class Client(object): +class Client(object): # pylint: disable=too-many-instance-attributes """ACME client. .. todo:: From 52d6e9b67435ee1d597c5a11474aba566e0aaaf3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 17 Jun 2015 16:13:26 +0000 Subject: [PATCH 080/109] acme-spec#118 revoke. --- acme/client.py | 14 +++++--------- acme/client_test.py | 5 +++-- acme/messages.py | 34 +++++++++++++++------------------- acme/messages_test.py | 31 +++++++++++++------------------ letsencrypt/revoker.py | 2 +- 5 files changed, 37 insertions(+), 49 deletions(-) diff --git a/acme/client.py b/acme/client.py index 629048d03..73c962581 100644 --- a/acme/client.py +++ b/acme/client.py @@ -539,21 +539,17 @@ class Client(object): # pylint: disable=too-many-instance-attributes else: return None - def revoke(self, certr, when=messages.Revocation.NOW): + def revoke(self, cert): """Revoke certificate. - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :param when: When should the revocation take place? Takes - the same values as `.messages.Revocation.revoke`. + :param .ComparableX509 body: `M2Crypto.X509.X509` wrapped in + `.ComparableX509` :raises .ClientError: If revocation is unsuccessful. """ - rev = messages.Revocation(revoke=when, authorizations=tuple( - authzr.uri for authzr in certr.authzrs)) - response = self._post(certr.uri, rev) + response = self._post(messages.Revocation.url(self.new_reg_uri), + messages.Revocation(certificate=cert)) if response.status_code != httplib.OK: raise errors.ClientError( 'Successful revocation must return HTTP OK status') diff --git a/acme/client_test.py b/acme/client_test.py index 5e4cc1720..dfa8d7607 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -517,8 +517,9 @@ class ClientTest(unittest.TestCase): def test_revoke(self): self._mock_post_get() - self.net.revoke(self.certr, when=messages.Revocation.NOW) - self.post.assert_called_once_with(self.certr.uri, mock.ANY) + self.net.revoke(self.certr.body) + self.post.assert_called_once_with(messages.Revocation.url( + self.net.new_reg_uri), mock.ANY) def test_revoke_bad_status_raises_error(self): self.response.status_code = httplib.METHOD_NOT_ALLOWED diff --git a/acme/messages.py b/acme/messages.py index aa041caed..bfc452a70 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -1,4 +1,6 @@ """ACME protocol messages.""" +import urlparse + from acme import challenges from acme import fields from acme import jose @@ -271,28 +273,22 @@ class CertificateResource(Resource): class Revocation(jose.JSONObjectWithFields): """Revocation message. - :ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`. - :ivar tuple authorizations: Same as `CertificateRequest.authorizations` + :ivar .ComparableX509 certificate: `M2Crypto.X509.X509` wrapped in + `.ComparableX509` """ + certificate = jose.Field( + 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) - NOW = 'now' - """A possible value for `revoke`, denoting that certificate should - be revoked now.""" + # TODO: acme-spec#138, this allows only one ACME server instance per domain + PATH = '/acme/revoke-cert' + """Path to revocation URL, see `url`""" - revoke = jose.Field('revoke') - authorizations = CertificateRequest._fields['authorizations'] + @classmethod + def url(cls, base): + """Get revocation URL. - @revoke.decoder - def revoke(value): # pylint: disable=missing-docstring,no-self-argument - if value == Revocation.NOW: - return value - else: - return fields.RFC3339Field.default_decoder(value) + :param str base: New Registration Resource or server (root) URL. - @revoke.encoder - def revoke(value): # pylint: disable=missing-docstring,no-self-argument - if value == Revocation.NOW: - return value - else: - return fields.RFC3339Field.default_encoder(value) + """ + return urlparse.urljoin(base, cls.PATH) diff --git a/acme/messages_test.py b/acme/messages_test.py index 4f86d7809..65c080ee7 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -1,11 +1,10 @@ """Tests for acme.messages.""" -import datetime import os import pkg_resources import unittest +import M2Crypto.X509 import mock -import pytz from Crypto.PublicKey import RSA from acme import challenges @@ -14,6 +13,9 @@ from acme import jose KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) +CERT = jose.ComparableX509(M2Crypto.X509.load_cert( + format=M2Crypto.X509.FORMAT_DER, file=pkg_resources.resource_filename( + 'acme.jose', os.path.join('testdata', 'cert.der')))) class ErrorTest(unittest.TestCase): @@ -223,27 +225,20 @@ class AuthorizationTest(unittest.TestCase): class RevocationTest(unittest.TestCase): """Tests for acme.messages.RevocationTest.""" + def test_url(self): + from acme.messages import Revocation + url = 'https://letsencrypt-demo.org/acme/revoke-cert' + self.assertEqual(url, Revocation.url('https://letsencrypt-demo.org')) + self.assertEqual( + url, Revocation.url('https://letsencrypt-demo.org/acme/new-reg')) + def setUp(self): from acme.messages import Revocation - self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW) - self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime( - 2015, 3, 27, tzinfo=pytz.utc)) - self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW} - self.jobj_date = {'authorizations': (), - 'revoke': '2015-03-27T00:00:00Z'} - - def test_revoke_decoder(self): - from acme.messages import Revocation - self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now)) - self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date)) - - def test_revoke_encoder(self): - self.assertEqual(self.jobj_now, self.rev_now.to_partial_json()) - self.assertEqual(self.jobj_date, self.rev_date.to_partial_json()) + self.rev = Revocation(certificate=CERT) def test_from_json_hashable(self): from acme.messages import Revocation - hash(Revocation.from_json(self.rev_now.to_json())) + hash(Revocation.from_json(self.rev.to_json())) if __name__ == '__main__': diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index 0d3bd8e79..402157721 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -253,7 +253,7 @@ class Revoker(object): raise errors.LetsEncryptRevokerError( "Corrupted backup key file: %s" % cert.backup_key_path) - return self.network.revoke(certr=None) # XXX + return self.network.revoke(cert=None) # XXX def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use """Remove certificate and key. From d970987b79c1f370ac1400ae9a31f01ee6f2722a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 20:30:17 +0000 Subject: [PATCH 081/109] Fix comment typo --- acme/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/client.py b/acme/client.py index 73c962581..1c0975849 100644 --- a/acme/client.py +++ b/acme/client.py @@ -542,7 +542,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes def revoke(self, cert): """Revoke certificate. - :param .ComparableX509 body: `M2Crypto.X509.X509` wrapped in + :param .ComparableX509 cert: `M2Crypto.X509.X509` wrapped in `.ComparableX509` :raises .ClientError: If revocation is unsuccessful. From e0a1e8f4e819cb1fad4d5760591264d03f2fb1ed Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 19:08:17 +0000 Subject: [PATCH 082/109] JSONDeSerializable acme.messages.Resource. Provides API necessary to implement JSON-based account storage as described at https://github.com/letsencrypt/lets-encrypt-preview/pull/362#issuecomment-97946817 --- acme/client.py | 1 + acme/client_test.py | 32 +++++------- acme/messages.py | 118 +++++++++++++++++++++++++++++++----------- acme/messages_test.py | 99 ++++++++++++++++++++++++++++++++++- 4 files changed, 197 insertions(+), 53 deletions(-) diff --git a/acme/client.py b/acme/client.py index 629048d03..43c659bb8 100644 --- a/acme/client.py +++ b/acme/client.py @@ -466,6 +466,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes updated_authzr, response = self.poll(updated[authzr]) updated[authzr] = updated_authzr + # pylint: disable=no-member if updated_authzr.body.status != messages.STATUS_VALID: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( diff --git a/acme/client_test.py b/acme/client_test.py index 5e4cc1720..7f09f8bdf 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -5,7 +5,6 @@ import os import pkg_resources import unittest -import M2Crypto import mock import requests @@ -14,16 +13,9 @@ from acme import errors from acme import jose from acme import jws as acme_jws from acme import messages +from acme import messages_test -CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( - pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'cert.der')), - M2Crypto.X509.FORMAT_DER)) -CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( - pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'csr.der')), - M2Crypto.X509.FORMAT_DER)) KEY = jose.JWKRSA.load(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( @@ -82,7 +74,7 @@ class ClientTest(unittest.TestCase): # Request issuance self.certr = messages.CertificateResource( - body=CERT, authzrs=(self.authzr,), + body=messages_test.CERT, authzrs=(self.authzr,), uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') @@ -380,27 +372,27 @@ class ClientTest(unittest.TestCase): self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) def test_request_issuance(self): - self.response.content = CERT.as_der() + self.response.content = messages_test.CERT.as_der() self.response.headers['Location'] = self.certr.uri self.response.links['up'] = {'url': self.certr.cert_chain_uri} self._mock_post_get() - self.assertEqual( - self.certr, self.net.request_issuance(CSR, (self.authzr,))) + self.assertEqual(self.certr, self.net.request_issuance( + messages_test.CSR, (self.authzr,))) # TODO: check POST args def test_request_issuance_missing_up(self): - self.response.content = CERT.as_der() + self.response.content = messages_test.CERT.as_der() self.response.headers['Location'] = self.certr.uri self._mock_post_get() self.assertEqual( self.certr.update(cert_chain_uri=None), - self.net.request_issuance(CSR, (self.authzr,))) + self.net.request_issuance(messages_test.CSR, (self.authzr,))) def test_request_issuance_missing_location(self): self._mock_post_get() self.assertRaises( errors.ClientError, self.net.request_issuance, - CSR, (self.authzr,)) + messages_test.CSR, (self.authzr,)) @mock.patch('acme.client.datetime') @mock.patch('acme.client.time') @@ -484,10 +476,10 @@ class ClientTest(unittest.TestCase): def test_check_cert(self): self.response.headers['Location'] = self.certr.uri - self.response.content = CERT.as_der() + self.response.content = messages_test.CERT.as_der() self._mock_post_get() - self.assertEqual( - self.certr.update(body=CERT), self.net.check_cert(self.certr)) + self.assertEqual(self.certr.update(body=messages_test.CERT), + self.net.check_cert(self.certr)) # TODO: split here and separate test self.response.headers['Location'] = 'foo' @@ -495,7 +487,7 @@ class ClientTest(unittest.TestCase): errors.UnexpectedUpdate, self.net.check_cert, self.certr) def test_check_cert_missing_location(self): - self.response.content = CERT.as_der() + self.response.content = messages_test.CERT.as_der() self._mock_post_get() self.assertRaises(errors.ClientError, self.net.check_cert, self.certr) diff --git a/acme/messages.py b/acme/messages.py index aa041caed..e355e9fd0 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -51,6 +51,7 @@ class Error(jose.JSONObjectWithFields, Exception): else: return str(self.detail) + class _Constant(jose.JSONDeSerializable): """ACME constant.""" __slots__ = ('name',) @@ -107,31 +108,29 @@ class Identifier(jose.JSONObjectWithFields): value = jose.Field('value') -class Resource(jose.ImmutableMap): +class Resource(jose.JSONObjectWithFields): """ACME Resource. + :ivar str uri: Location of the resource. :ivar acme.messages.ResourceBody body: Resource body. + + """ + body = jose.Field('body') + + +class ResourceWithURI(Resource): + """ACME Resource with URI. + :ivar str uri: Location of the resource. """ - __slots__ = ('body', 'uri') + uri = jose.Field('uri') # no ChallengeResource.uri class ResourceBody(jose.JSONObjectWithFields): """ACME Resource Body.""" -class RegistrationResource(Resource): - """Registration Resource. - - :ivar acme.messages.Registration body: - :ivar str new_authzr_uri: URI found in the 'next' ``Link`` header - :ivar str terms_of_service: URL for the CA TOS. - - """ - __slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service') - - class Registration(ResourceBody): """Registration Resource Body. @@ -146,21 +145,59 @@ class Registration(ResourceBody): recovery_token = jose.Field('recoveryToken', omitempty=True) agreement = jose.Field('agreement', omitempty=True) + phone_prefix = 'tel:' + email_prefix = 'mailto:' -class ChallengeResource(Resource, jose.JSONObjectWithFields): - """Challenge Resource. + @classmethod + def from_data(cls, phone=None, email=None, **kwargs): + """Create registration resource from contact detauls.""" + details = list(kwargs.pop('contact', ())) + if phone is not None: + details.append(cls.phone_prefix + phone) + if email is not None: + details.append(cls.email_prefix + email) + kwargs['contact'] = tuple(details) + return cls(**kwargs) - :ivar acme.messages.ChallengeBody body: - :ivar str authzr_uri: URI found in the 'up' ``Link`` header. - - """ - __slots__ = ('body', 'authzr_uri') + def _filter_contact(self, prefix): + return tuple( + detail[len(prefix):] for detail in self.contact + if detail.startswith(prefix)) @property - def uri(self): # pylint: disable=missing-docstring,no-self-argument - # bug? 'method already defined line None' - # pylint: disable=function-redefined - return self.body.uri + def phones(self): + """All phones found in the ``contact`` field.""" + return self._filter_contact(self.phone_prefix) + + @property + def emails(self): + """All emails found in the ``contact`` field.""" + return self._filter_contact(self.email_prefix) + + @property + def phone(self): + """Phone.""" + assert len(self.phones) == 1 + return self.phones[0] + + @property + def email(self): + """Email.""" + assert len(self.emails) == 1 + return self.emails[0] + + +class RegistrationResource(ResourceWithURI): + """Registration Resource. + + :ivar acme.messages.Registration body: + :ivar str new_authzr_uri: URI found in the 'next' ``Link`` header + :ivar str terms_of_service: URL for the CA TOS. + + """ + body = jose.Field('body', decoder=Registration.from_json) + new_authzr_uri = jose.Field('new_authzr_uri') + terms_of_service = jose.Field('terms_of_service', omitempty=True) class ChallengeBody(ResourceBody): @@ -199,14 +236,21 @@ class ChallengeBody(ResourceBody): return getattr(self.chall, name) -class AuthorizationResource(Resource): - """Authorization Resource. +class ChallengeResource(Resource, jose.JSONObjectWithFields): + """Challenge Resource. - :ivar acme.messages.Authorization body: - :ivar str new_cert_uri: URI found in the 'next' ``Link`` header + :ivar acme.messages.ChallengeBody body: + :ivar str authzr_uri: URI found in the 'up' ``Link`` header. """ - __slots__ = ('body', 'uri', 'new_cert_uri') + body = jose.Field('body', decoder=ChallengeBody.from_json) + authzr_uri = jose.Field('authzr_uri') + + @property + def uri(self): # pylint: disable=missing-docstring,no-self-argument + # bug? 'method already defined line None' + # pylint: disable=function-redefined + return self.body.uri # pylint: disable=no-member class Authorization(ResourceBody): @@ -244,6 +288,17 @@ class Authorization(ResourceBody): for combo in self.combinations) +class AuthorizationResource(ResourceWithURI): + """Authorization Resource. + + :ivar acme.messages.Authorization body: + :ivar str new_cert_uri: URI found in the 'next' ``Link`` header + + """ + body = jose.Field('body', decoder=Authorization.from_json) + new_cert_uri = jose.Field('new_cert_uri') + + class CertificateRequest(jose.JSONObjectWithFields): """ACME new-cert request. @@ -256,7 +311,7 @@ class CertificateRequest(jose.JSONObjectWithFields): authorizations = jose.Field('authorizations', decoder=tuple) -class CertificateResource(Resource): +class CertificateResource(ResourceWithURI): """Certificate Resource. :ivar acme.jose.util.ComparableX509 body: @@ -265,7 +320,8 @@ class CertificateResource(Resource): :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ - __slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs') + cert_chain_uri = jose.Field('cert_chain_uri') + authzrs = jose.Field('authzrs') class Revocation(jose.JSONObjectWithFields): diff --git a/acme/messages_test.py b/acme/messages_test.py index 4f86d7809..6749f55dc 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -4,14 +4,23 @@ import os import pkg_resources import unittest +from Crypto.PublicKey import RSA +import M2Crypto import mock import pytz -from Crypto.PublicKey import RSA from acme import challenges from acme import jose +CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( + pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'cert.der')), + M2Crypto.X509.FORMAT_DER)) +CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( + pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'csr.der')), + M2Crypto.X509.FORMAT_DER)) KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) @@ -94,12 +103,16 @@ class ConstantTest(unittest.TestCase): self.assertTrue(self.const_a != self.const_b) self.assertFalse(self.const_a != const_a_prime) + class RegistrationTest(unittest.TestCase): """Tests for acme.messages.Registration.""" def setUp(self): key = jose.jwk.JWKRSA(key=KEY.publickey()) - contact = ('mailto:letsencrypt-client@letsencrypt.org',) + contact = ( + 'mailto:admin@foo.com', + 'tel:1234', + ) recovery_token = 'XYZ' agreement = 'https://letsencrypt.org/terms' @@ -117,6 +130,26 @@ class RegistrationTest(unittest.TestCase): self.jobj_from = self.jobj_to.copy() self.jobj_from['key'] = key.to_json() + def test_from_data(self): + from acme.messages import Registration + reg = Registration.from_data(phone='1234', email='admin@foo.com') + self.assertEqual(reg.contact, ( + 'tel:1234', + 'mailto:admin@foo.com', + )) + + def test_phones(self): + self.assertEqual(('1234',), self.reg.phones) + + def test_emails(self): + self.assertEqual(('admin@foo.com',), self.reg.emails) + + def test_phone(self): + self.assertEqual('1234', self.reg.phone) + + def test_email(self): + self.assertEqual('admin@foo.com', self.reg.email) + def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) @@ -129,6 +162,25 @@ class RegistrationTest(unittest.TestCase): hash(Registration.from_json(self.jobj_from)) +class RegistrationResourceTest(unittest.TestCase): + """Tests for acme.messages.RegistrationResource.""" + + def setUp(self): + from acme.messages import RegistrationResource + self.regr = RegistrationResource( + body=mock.sentinel.body, uri=mock.sentinel.uri, + new_authzr_uri=mock.sentinel.new_authzr_uri, + terms_of_service=mock.sentinel.terms_of_service) + + def test_to_partial_json(self): + self.assertEqual(self.regr.to_json(), { + 'body': mock.sentinel.body, + 'uri': mock.sentinel.uri, + 'new_authzr_uri': mock.sentinel.new_authzr_uri, + 'terms_of_service': mock.sentinel.terms_of_service, + }) + + class ChallengeResourceTest(unittest.TestCase): """Tests for acme.messages.ChallengeResource.""" @@ -220,6 +272,49 @@ class AuthorizationTest(unittest.TestCase): )) +class AuthorizationResourceTest(unittest.TestCase): + """Tests for acme.messages.AuthorizationResource.""" + + def test_json_de_serializable(self): + from acme.messages import AuthorizationResource + authzr = AuthorizationResource( + uri=mock.sentinel.uri, + body=mock.sentinel.body, + new_cert_uri=mock.sentinel.new_cert_uri, + ) + self.assertTrue(isinstance(authzr, jose.JSONDeSerializable)) + + +class CertificateRequestTest(unittest.TestCase): + """Tests for acme.messages.CertificateRequest.""" + + def setUp(self): + from acme.messages import CertificateRequest + self.req = CertificateRequest(csr=CSR, authorizations=('foo',)) + + def test_json_de_serializable(self): + self.assertTrue(isinstance(self.req, jose.JSONDeSerializable)) + from acme.messages import CertificateRequest + self.assertEqual( + self.req, CertificateRequest.from_json(self.req.to_json())) + + +class CertificateResourceTest(unittest.TestCase): + """Tests for acme.messages.CertificateResourceTest.""" + + def setUp(self): + from acme.messages import CertificateResource + self.certr = CertificateResource( + body=CERT, uri=mock.sentinel.uri, authzrs=(), + cert_chain_uri=mock.sentinel.cert_chain_uri) + + def test_json_de_serializable(self): + self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable)) + from acme.messages import CertificateResource + self.assertEqual( + self.certr, CertificateResource.from_json(self.certr.to_json())) + + class RevocationTest(unittest.TestCase): """Tests for acme.messages.RevocationTest.""" From c5bf2730246bdf6fcea1747eb0b39445deb6c113 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 08:01:28 +0000 Subject: [PATCH 083/109] setup.py: separate install_requires --- setup.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/setup.py b/setup.py index 150dfb24d..ef819f50b 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,61 @@ 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')) +# #358: acme, letsencrypt, letsencrypt_apache, letsencrypt_nginx, etc. +# shall be distributed separately. Please make sure to keep the +# dependecy lists up to date: this is being somewhat checked below +# using an assert statement! Separate lists are helpful for OS package +# maintainers. and will make the future migration a lot easier. +acme_install_requires = [ + 'argparse', + #'letsencrypt' # TODO: uses testdata vectors + 'mock', + 'pycrypto', + 'pyrfc3339', + 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) + 'pyasn1', # urllib3 InsecurePlatformWarning (#304) + 'pytz', + 'requests', + 'werkzeug', + 'M2Crypto', +] +letsencrypt_install_requires = [ + #'acme', + 'argparse', + 'ConfigArgParse', + 'configobj', + 'M2Crypto', + 'mock', + 'parsedatetime', + 'psutil>=2.1.0', # net_connections introduced in 2.1.0 + 'pycrypto', + # https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions + 'PyOpenSSL>=0.15', + 'pyrfc3339', + 'python-augeas', + 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 + 'pytz', + 'requests', + 'zope.component', + 'zope.interface', + 'M2Crypto', +] +letsencrypt_apache_install_requires = [ + #'acme', + #'letsencrypt', + 'mock', + 'python-augeas', + 'zope.component', + 'zope.interface', +] +letsencrypt_nginx_install_requires = [ + #'acme', + #'letsencrypt', + 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? + 'mock', + 'zope.interface', +] + install_requires = [ 'argparse', 'ConfigArgParse', @@ -54,6 +109,13 @@ install_requires = [ 'M2Crypto', ] +assert set(install_requires) == set.union(*(set(ireq) for ireq in ( + acme_install_requires, + letsencrypt_install_requires, + letsencrypt_apache_install_requires, + letsencrypt_nginx_install_requires +))), "*install_requires don't match up!" + dev_extras = [ # Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289 'astroid==1.3.5', From b6ef25e911f8304d7a2a22f615ed7a0187eb9914 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 21:06:08 +0000 Subject: [PATCH 084/109] Fix review comments (typo, inheritance fix). --- acme/messages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/messages.py b/acme/messages.py index cd5a65f79..c6d15bbf1 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -152,7 +152,7 @@ class Registration(ResourceBody): @classmethod def from_data(cls, phone=None, email=None, **kwargs): - """Create registration resource from contact detauls.""" + """Create registration resource from contact details.""" details = list(kwargs.pop('contact', ())) if phone is not None: details.append(cls.phone_prefix + phone) @@ -238,7 +238,7 @@ class ChallengeBody(ResourceBody): return getattr(self.chall, name) -class ChallengeResource(Resource, jose.JSONObjectWithFields): +class ChallengeResource(Resource): """Challenge Resource. :ivar acme.messages.ChallengeBody body: From e17bd684bb0e3d66650ad264f9c37950def75b76 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 07:55:22 +0000 Subject: [PATCH 085/109] Debug log received response for GET/POST --- acme/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/acme/client.py b/acme/client.py index 1c0975849..7e0e3ebd1 100644 --- a/acme/client.py +++ b/acme/client.py @@ -77,6 +77,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes :raises .ClientError: In case of other networking errors. """ + logging.debug('Received response %s (headers: %s): %r', + response, response.headers, response.content) + response_ct = response.headers.get('Content-Type') try: # TODO: response.json() is called twice, once here, and @@ -169,7 +172,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes response = requests.post(uri, data=data, **kwargs) except requests.exceptions.RequestException as error: raise errors.ClientError(error) - logging.debug('Received response %s: %r', response, response.text) self._add_nonce(response) self._check_response(response, content_type=content_type) From 28f5c7d6665b004290e99aad5a51fcdc73edec95 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 10:11:59 +0000 Subject: [PATCH 086/109] logs: collate omitted empty fields --- acme/jose/json_util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/acme/jose/json_util.py b/acme/jose/json_util.py index a08145459..f38ebc62f 100644 --- a/acme/jose/json_util.py +++ b/acme/jose/json_util.py @@ -218,11 +218,12 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable): def fields_to_partial_json(self): """Serialize fields to JSON.""" jobj = {} + omitted = set() for slot, field in self._fields.iteritems(): value = getattr(self, slot) if field.omit(value): - logging.debug('Omitting empty field "%s" (%s)', slot, value) + omitted.add((slot, value)) else: try: jobj[field.json_name] = field.encode(value) @@ -230,6 +231,10 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable): raise errors.SerializationError( 'Could not encode {0} ({1}): {2}'.format( slot, value, error)) + if omitted: + # pylint: disable=star-args + logging.debug('Omitted empty fields: %s', ', '.join( + '{0!s}={1!r}'.format(*field) for field in omitted)) return jobj def to_partial_json(self): From 8e39a3a0ef530b35ee028b0902deeca7c29f369c Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 13 Jun 2015 14:36:51 +0000 Subject: [PATCH 087/109] Collate multi-line logs, use logging.exception, other fixes. --- acme/client.py | 2 -- letsencrypt/account.py | 2 +- letsencrypt/augeas_configurator.py | 19 ++++++++----------- letsencrypt/auth_handler.py | 8 +------- letsencrypt/crypto_util.py | 2 +- letsencrypt_apache/configurator.py | 21 ++++++++------------- letsencrypt_apache/dvsni.py | 7 +++---- letsencrypt_nginx/configurator.py | 16 +++++----------- letsencrypt_nginx/dvsni.py | 4 ++-- 9 files changed, 29 insertions(+), 52 deletions(-) diff --git a/acme/client.py b/acme/client.py index 7e0e3ebd1..fe4c42799 100644 --- a/acme/client.py +++ b/acme/client.py @@ -95,8 +95,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes 'Ignoring wrong Content-Type (%r) for JSON Error', response_ct) try: - logging.error("Error: %s", jobj) - logging.error("Response from server: %s", response.content) raise messages.Error.from_json(jobj) except jose.DeserializationError as error: # Couldn't deserialize JSON object diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 9f351387f..a97e07504 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -227,5 +227,5 @@ class Account(object): if cls.EMAIL_REGEX.match(email): return not email.startswith(".") and ".." not in email else: - logging.warn("Invalid email address.") + logging.warn("Invalid email address: %s.", email) return False diff --git a/letsencrypt/augeas_configurator.py b/letsencrypt/augeas_configurator.py index c59d755c2..a375b2e17 100644 --- a/letsencrypt/augeas_configurator.py +++ b/letsencrypt/augeas_configurator.py @@ -52,10 +52,10 @@ class AugeasConfigurator(common.Plugin): lens_path = self.aug.get(path + "/lens") # As aug.get may return null if lens_path and lens in lens_path: - # Strip off /augeas/files and /error - logging.error("There has been an error in parsing the file: %s", - path[13:len(path) - 6]) - logging.error(self.aug.get(path + "/message")) + logging.error( + "There has been an error in parsing the file (%s): %s", + # Strip off /augeas/files and /error + path[13:len(path) - 6], self.aug.get(path + "/message")) def save(self, title=None, temporary=False): """Saves all changes to the configuration files. @@ -122,13 +122,10 @@ class AugeasConfigurator(common.Plugin): # Check for the root of save problems new_errs = self.aug.match("/augeas//error") # logging.error("During Save - %s", mod_conf) - # Only print new errors caused by recent save - for err in new_errs: - if err not in ex_errs: - logging.error( - "Unable to save file - %s", err[13:len(err) - 6]) - logging.error("Attempted Save Notes") - logging.error(self.save_notes) + logging.error("Unable to save files: %s. Attempted Save Notes: %s", + ", ".join(err[13:len(err) - 6] for err in new_errs + # Only new errors caused by recent save + if err not in ex_errs), self.save_notes) # Wrapper functions for Reverter class def recovery_routine(self): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index d895c165c..50a66c0d0 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -333,28 +333,22 @@ def challb_to_achall(challb, key, domain): """ chall = challb.chall + logging.info("%s challenge for %s", chall.typ, domain) if isinstance(chall, challenges.DVSNI): - logging.info(" DVSNI challenge for %s.", domain) return achallenges.DVSNI( challb=challb, domain=domain, key=key) elif isinstance(chall, challenges.SimpleHTTP): - logging.info(" SimpleHTTP challenge for %s.", domain) return achallenges.SimpleHTTP( challb=challb, domain=domain, key=key) elif isinstance(chall, challenges.DNS): - logging.info(" DNS challenge for %s.", domain) return achallenges.DNS(challb=challb, domain=domain) - elif isinstance(chall, challenges.RecoveryToken): - logging.info(" Recovery Token Challenge for %s.", domain) return achallenges.RecoveryToken(challb=challb, domain=domain) elif isinstance(chall, challenges.RecoveryContact): - logging.info(" Recovery Contact Challenge for %s.", domain) return achallenges.RecoveryContact( challb=challb, domain=domain) elif isinstance(chall, challenges.ProofOfPossession): - logging.info(" Proof-of-Possession Challenge for %s", domain) return achallenges.ProofOfPossession( challb=challb, domain=domain) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 1eb565289..7e1bb58fb 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -40,7 +40,7 @@ def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"): try: key_pem = make_key(key_size) except ValueError as err: - logging.fatal(str(err)) + logging.exception(err) raise err # Save file diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 0cff94bbd..5b0dbdea9 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -181,8 +181,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not path["cert_path"] or not path["cert_key"]: # Throw some can't find all of the directives error" logging.warn( - "Cannot find a cert or key directive in %s", vhost.path) - logging.warn("VirtualHost was not modified") + "Cannot find a cert or key directive in %s. " + "VirtualHost was not modified", vhost.path) # Presumably break here so that the virtualhost is not modified return False @@ -408,8 +408,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Note: This could be made to also look for ip:443 combo # TODO: Need to search only open directives and IfMod mod_ssl.c if len(self.parser.find_dir(parser.case_i("Listen"), "443")) == 0: - logging.debug("No Listen 443 directive found") - logging.debug("Setting the Apache Server to Listen on port 443") + logging.debug("No Listen 443 directive found. Setting the " + "Apache Server to Listen on port 443") path = self.parser.add_dir_to_ifmodssl( parser.get_aug_path(self.parser.loc["listen"]), "Listen", "443") self.save_notes += "Added Listen 443 directive to %s\n" % path @@ -922,9 +922,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if proc.returncode != 0: # Enter recovery routine... - logging.error("Configtest failed") - logging.error(stdout) - logging.error(stderr) + logging.error("Configtest failed\n%s\n%s", stdout, stderr) return False return True @@ -1054,9 +1052,8 @@ def enable_mod(mod_name, apache_init_script, apache_enmod): stdout=open("/dev/null", "w"), stderr=open("/dev/null", "w")) apache_restart(apache_init_script) - except (OSError, subprocess.CalledProcessError) as err: - logging.error("Error enabling mod_%s", mod_name) - logging.error("Exception: %s", err) + except (OSError, subprocess.CalledProcessError): + logging.exception("Error enabling mod_%s", mod_name) sys.exit(1) @@ -1119,9 +1116,7 @@ def apache_restart(apache_init_script): if proc.returncode != 0: # Enter recovery routine... - logging.error("Apache Restart Failed!") - logging.error(stdout) - logging.error(stderr) + logging.error("Apache Restart Failed!\n%s\n%s", stdout, stderr) return False except (OSError, ValueError): diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py index c25426371..5ff09aa50 100644 --- a/letsencrypt_apache/dvsni.py +++ b/letsencrypt_apache/dvsni.py @@ -59,10 +59,9 @@ class ApacheDvsni(common.Dvsni): vhost = self.configurator.choose_vhost(achall.domain) if vhost is None: logging.error( - "No vhost exists with servername or alias of: %s", - achall.domain) - logging.error("No _default_:443 vhost exists") - logging.error("Please specify servernames in the Apache config") + "No vhost exists with servername or alias of: %s. " + "No _default_:443 vhost exists. Please specify servernames " + "in the Apache config", achall.domain) return None # TODO - @jdkasten review this code to make sure it makes sense diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index f7b53f3fa..f74ad0a3a 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -130,9 +130,8 @@ class NginxConfigurator(common.Plugin): vhost.filep, vhost.names) except errors.LetsEncryptMisconfigurationError: logging.warn( - "Cannot find a cert or key directive in %s for %s", - vhost.filep, vhost.names) - logging.warn("VirtualHost was not modified") + "Cannot find a cert or key directive in %s for %s. " + "VirtualHost was not modified.", vhost.filep, vhost.names) # Presumably break here so that the virtualhost is not modified return False @@ -352,9 +351,7 @@ class NginxConfigurator(common.Plugin): if proc.returncode != 0: # Enter recovery routine... - logging.error("Config test failed") - logging.error(stdout) - logging.error(stderr) + logging.error("Config test failed\n%s\n%s", stdout, stderr) return False return True @@ -570,14 +567,11 @@ def nginx_restart(nginx_ctl): if nginx_proc.returncode != 0: # Enter recovery routine... - logging.error("Nginx Restart Failed!") - logging.error(stdout) - logging.error(stderr) + logging.error("Nginx Restart Failed!\n%s\n%s", stdout, stderr) return False except (OSError, ValueError): - logging.fatal( - "Nginx Restart Failed - Please Check the Configuration") + logging.fatal("Nginx Restart Failed - Please Check the Configuration") sys.exit(1) return True diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 0697f6e1e..f6f82c5cb 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -50,9 +50,9 @@ class NginxDvsni(common.Dvsni): vhost = self.configurator.choose_vhost(achall.domain) if vhost is None: logging.error( - "No nginx vhost exists with server_name matching: %s", + "No nginx vhost exists with server_name matching: %s. " + "Please specify server_names in the Nginx config.", achall.domain) - logging.error("Please specify server_names in the Nginx config") return None for addr in vhost.addrs: From cfa7e281065419227454ee2dc1b90d7bdf8d0419 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 14:10:39 +0000 Subject: [PATCH 088/109] errors.LetsEncrypt -> errors. (fixes: #487) --- letsencrypt/account.py | 8 +-- letsencrypt/auth_handler.py | 4 +- letsencrypt/cli.py | 6 +-- letsencrypt/client.py | 22 ++++---- letsencrypt/continuity_auth.py | 4 +- letsencrypt/display/enhancements.py | 4 +- letsencrypt/errors.py | 20 +++---- letsencrypt/interfaces.py | 4 +- letsencrypt/le_util.py | 4 +- letsencrypt/plugins/disco.py | 6 +-- letsencrypt/plugins/disco_test.py | 8 +-- letsencrypt/reverter.py | 52 +++++++++---------- letsencrypt/revoker.py | 12 ++--- letsencrypt/tests/account_test.py | 6 +-- letsencrypt/tests/auth_handler_test.py | 2 +- letsencrypt/tests/continuity_auth_test.py | 4 +- .../tests/display/enhancements_test.py | 2 +- letsencrypt/tests/le_util_test.py | 2 +- letsencrypt/tests/reverter_test.py | 40 +++++++------- letsencrypt/tests/revoker_test.py | 12 ++--- letsencrypt_apache/configurator.py | 18 +++---- letsencrypt_apache/parser.py | 2 +- letsencrypt_apache/tests/configurator_test.py | 6 +-- letsencrypt_apache/tests/parser_test.py | 2 +- letsencrypt_nginx/configurator.py | 18 +++---- letsencrypt_nginx/dvsni.py | 4 +- letsencrypt_nginx/parser.py | 4 +- letsencrypt_nginx/tests/configurator_test.py | 12 ++--- letsencrypt_nginx/tests/dvsni_test.py | 2 +- letsencrypt_nginx/tests/parser_test.py | 4 +- 30 files changed, 147 insertions(+), 147 deletions(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index a97e07504..e6e46d098 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -127,7 +127,7 @@ class Account(object): acc_config = configobj.ConfigObj( infile=config_fp, file_error=True, create_empty=False) except IOError: - raise errors.LetsEncryptClientError( + raise errors.Error( "Account for %s does not exist" % os.path.basename(config_fp)) if os.path.basename(config_fp) != "default": @@ -191,7 +191,7 @@ class Account(object): if code == display_util.OK: try: return cls.from_email(config, email) - except errors.LetsEncryptClientError: + except errors.Error: continue else: return None @@ -205,7 +205,7 @@ class Account(object): :param str email: Email address - :raises letsencrypt.errors.LetsEncryptClientError: If invalid + :raises letsencrypt.errors.Error: If invalid email address is given. """ @@ -219,7 +219,7 @@ class Account(object): cls._get_config_filename(email)) return cls(config, key, email) - raise errors.LetsEncryptClientError("Invalid email address.") + raise errors.Error("Invalid email address.") @classmethod def safe_email(cls, email): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 50a66c0d0..a86cdd69d 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -296,7 +296,7 @@ class AuthHandler(object): :class:`letsencrypt.achallenges.Indexed` :rtype: tuple - :raises errors.LetsEncryptClientError: If Challenge type is not + :raises errors.Error: If Challenge type is not recognized """ @@ -353,7 +353,7 @@ def challb_to_achall(challb, key, domain): challb=challb, domain=domain) else: - raise errors.LetsEncryptClientError( + raise errors.Error( "Received unsupported challenge of type: %s", chall.typ) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 3bdf2bfc6..f04727706 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -41,11 +41,11 @@ def _account_init(args, config): # The way to get the default would be args.email = "" # First try existing account return account.Account.from_existing_account(config, args.email) - except errors.LetsEncryptClientError: + except errors.Error: try: # Try to make an account based on the email address return account.Account.from_email(config, args.email) - except errors.LetsEncryptClientError: + except errors.Error: return None @@ -68,7 +68,7 @@ def _common_run(args, config, acc, authenticator, installer): if acc.regr is None: try: acme.register() - except errors.LetsEncryptClientError: + except errors.Error: sys.exit("Unable to register an account with ACME server") return acme, doms diff --git a/letsencrypt/client.py b/letsencrypt/client.py index d059a777e..e29064a56 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -96,7 +96,7 @@ class Client(object): self.account.regr = self.network.agree_to_tos(self.account.regr) else: # What is the proper response here... - raise errors.LetsEncryptClientError("Must agree to TOS") + raise errors.Error("Must agree to TOS") self.account.save() self._report_new_account() @@ -145,9 +145,9 @@ class Client(object): msg = ("Unable to obtain certificate because authenticator is " "not set.") logging.warning(msg) - raise errors.LetsEncryptClientError(msg) + raise errors.Error(msg) if self.account.regr is None: - raise errors.LetsEncryptClientError( + raise errors.Error( "Please register with the ACME server first.") # Perform Challenges/Get Authorizations @@ -310,7 +310,7 @@ class Client(object): if self.installer is None: logging.warning("No installer specified, client is unable to deploy" "the certificate") - raise errors.LetsEncryptClientError("No installer available") + raise errors.Error("No installer available") chain_path = None if chain_path is None else os.path.abspath(chain_path) @@ -339,14 +339,14 @@ class Client(object): :param redirect: If traffic should be forwarded from HTTP to HTTPS. :type redirect: bool or None - :raises letsencrypt.errors.LetsEncryptClientError: if + :raises letsencrypt.errors.Error: if no installer is specified in the client. """ if self.installer is None: logging.warning("No installer is specified, there isn't any " "configuration to enhance.") - raise errors.LetsEncryptClientError("No installer available") + raise errors.Error("No installer available") if redirect is None: redirect = enhancements.ask("redirect") @@ -364,7 +364,7 @@ class Client(object): for dom in domains: try: self.installer.enhance(dom, "redirect") - except errors.LetsEncryptConfiguratorError: + except errors.ConfiguratorError: logging.warn("Unable to perform redirect for %s", dom) self.installer.save("Add Redirects") @@ -386,7 +386,7 @@ def validate_key_csr(privkey, csr=None): :param csr: CSR :type csr: :class:`letsencrypt.le_util.CSR` - :raises letsencrypt.errors.LetsEncryptClientError: when + :raises letsencrypt.errors.Error: when validation fails """ @@ -396,7 +396,7 @@ def validate_key_csr(privkey, csr=None): # Key must be readable and valid. if privkey.pem and not crypto_util.valid_privkey(privkey.pem): - raise errors.LetsEncryptClientError( + raise errors.Error( "The provided key is not a valid key") if csr: @@ -406,7 +406,7 @@ def validate_key_csr(privkey, csr=None): # If CSR is provided, it must be readable and valid. if csr.data and not crypto_util.valid_csr(csr.data): - raise errors.LetsEncryptClientError( + raise errors.Error( "The provided CSR is not a valid CSR") # If both CSR and key are provided, the key must be the same key used @@ -414,7 +414,7 @@ def validate_key_csr(privkey, csr=None): if csr.data and privkey.pem: if not crypto_util.csr_matches_pubkey( csr.data, privkey.pem): - raise errors.LetsEncryptClientError( + raise errors.Error( "The key and CSR do not match") diff --git a/letsencrypt/continuity_auth.py b/letsencrypt/continuity_auth.py index 739e33d43..2eb1c22bf 100644 --- a/letsencrypt/continuity_auth.py +++ b/letsencrypt/continuity_auth.py @@ -52,7 +52,7 @@ class ContinuityAuthenticator(object): elif isinstance(achall, achallenges.RecoveryToken): responses.append(self.rec_token.perform(achall)) else: - raise errors.LetsEncryptContAuthError("Unexpected Challenge") + raise errors.ContAuthError("Unexpected Challenge") return responses def cleanup(self, achalls): @@ -61,4 +61,4 @@ class ContinuityAuthenticator(object): if isinstance(achall, achallenges.RecoveryToken): self.rec_token.cleanup(achall) elif not isinstance(achall, achallenges.ProofOfPossession): - raise errors.LetsEncryptContAuthError("Unexpected Challenge") + raise errors.ContAuthError("Unexpected Challenge") diff --git a/letsencrypt/display/enhancements.py b/letsencrypt/display/enhancements.py index 48f168441..7855b7fba 100644 --- a/letsencrypt/display/enhancements.py +++ b/letsencrypt/display/enhancements.py @@ -21,7 +21,7 @@ def ask(enhancement): :returns: True if feature is desired, False otherwise :rtype: bool - :raises letsencrypt.errors.LetsEncryptClientError: If + :raises letsencrypt.errors.Error: If the enhancement provided is not supported. """ @@ -30,7 +30,7 @@ def ask(enhancement): return DISPATCH[enhancement]() except KeyError: logging.error("Unsupported enhancement given to ask(): %s", enhancement) - raise errors.LetsEncryptClientError("Unsupported Enhancement") + raise errors.Error("Unsupported Enhancement") def redirect_by_default(): diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index d9078dbf2..85f4a69d9 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -1,44 +1,44 @@ """Let's Encrypt client errors.""" -class LetsEncryptClientError(Exception): +class Error(Exception): """Generic Let's Encrypt client error.""" -class LetsEncryptReverterError(LetsEncryptClientError): +class ReverterError(Error): """Let's Encrypt Reverter error.""" # Auth Handler Errors -class AuthorizationError(LetsEncryptClientError): +class AuthorizationError(Error): """Authorization error.""" -class LetsEncryptContAuthError(AuthorizationError): +class ContAuthError(AuthorizationError): """Let's Encrypt Continuity Authenticator error.""" -class LetsEncryptDvAuthError(AuthorizationError): +class DvAuthError(AuthorizationError): """Let's Encrypt DV Authenticator error.""" # Authenticator - Challenge specific errors -class LetsEncryptDvsniError(LetsEncryptDvAuthError): +class DvsniError(DvAuthError): """Let's Encrypt DVSNI error.""" # Configurator Errors -class LetsEncryptConfiguratorError(LetsEncryptClientError): +class ConfiguratorError(Error): """Let's Encrypt Configurator error.""" -class LetsEncryptNoInstallationError(LetsEncryptConfiguratorError): +class NoInstallationError(ConfiguratorError): """Let's Encrypt No Installation error.""" -class LetsEncryptMisconfigurationError(LetsEncryptConfiguratorError): +class MisconfigurationError(ConfiguratorError): """Let's Encrypt Misconfiguration error.""" -class LetsEncryptRevokerError(LetsEncryptClientError): +class RevokerError(Error): """Let's Encrypt Revoker error.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index d2a420d00..539683d30 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -68,10 +68,10 @@ class IPlugin(zope.interface.Interface): Finish up any additional initialization. - :raises letsencrypt.errors.LetsEncryptMisconfigurationError: + :raises letsencrypt.errors.MisconfigurationError: when full initialization cannot be completed. Plugin will be displayed on a list of available plugins. - :raises letsencrypt.errors.LetsEncryptNoInstallationError: + :raises letsencrypt.errors.NoInstallationError: when the necessary programs/files cannot be located. Plugin will NOT be displayed on a list of available plugins. diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index ba2427c79..2b0a4a495 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -19,7 +19,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): :param int mode: Directory mode. :param int uid: Directory owner. - :raises LetsEncryptClientError: if a directory already exists, + :raises Error: if a directory already exists, but has wrong permissions or owner :raises OSError: if invalid or inaccessible file names and @@ -32,7 +32,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): except OSError as exception: if exception.errno == errno.EEXIST: if not check_permissions(directory, mode, uid): - raise errors.LetsEncryptClientError( + raise errors.Error( "%s exists, but does not have the proper " "permissions or owner" % directory) else: diff --git a/letsencrypt/plugins/disco.py b/letsencrypt/plugins/disco.py index d70dfc751..229e152e2 100644 --- a/letsencrypt/plugins/disco.py +++ b/letsencrypt/plugins/disco.py @@ -89,10 +89,10 @@ class PluginEntryPoint(object): if self._prepared is None: try: self._initialized.prepare() - except errors.LetsEncryptMisconfigurationError as error: + except errors.MisconfigurationError as error: logging.debug("Misconfigured %r: %s", self, error) self._prepared = error - except errors.LetsEncryptNoInstallationError as error: + except errors.NoInstallationError as error: logging.debug("No installation (%r): %s", self, error) self._prepared = error else: @@ -103,7 +103,7 @@ class PluginEntryPoint(object): def misconfigured(self): """Is plugin misconfigured?""" return isinstance( - self._prepared, errors.LetsEncryptMisconfigurationError) + self._prepared, errors.MisconfigurationError) @property def available(self): diff --git a/letsencrypt/plugins/disco_test.py b/letsencrypt/plugins/disco_test.py index 0dd65e5de..1cd74385e 100644 --- a/letsencrypt/plugins/disco_test.py +++ b/letsencrypt/plugins/disco_test.py @@ -124,22 +124,22 @@ class PluginEntryPointTest(unittest.TestCase): def test_prepare_misconfigured(self): plugin = mock.MagicMock() - plugin.prepare.side_effect = errors.LetsEncryptMisconfigurationError + plugin.prepare.side_effect = errors.MisconfigurationError # pylint: disable=protected-access self.plugin_ep._initialized = plugin self.assertTrue(isinstance(self.plugin_ep.prepare(), - errors.LetsEncryptMisconfigurationError)) + errors.MisconfigurationError)) self.assertTrue(self.plugin_ep.prepared) self.assertTrue(self.plugin_ep.misconfigured) self.assertTrue(self.plugin_ep.available) def test_prepare_no_installation(self): plugin = mock.MagicMock() - plugin.prepare.side_effect = errors.LetsEncryptNoInstallationError + plugin.prepare.side_effect = errors.NoInstallationError # pylint: disable=protected-access self.plugin_ep._initialized = plugin self.assertTrue(isinstance(self.plugin_ep.prepare(), - errors.LetsEncryptNoInstallationError)) + errors.NoInstallationError)) self.assertTrue(self.plugin_ep.prepared) self.assertFalse(self.plugin_ep.misconfigured) self.assertFalse(self.plugin_ep.available) diff --git a/letsencrypt/reverter.py b/letsencrypt/reverter.py index 604c3999a..2743be97e 100644 --- a/letsencrypt/reverter.py +++ b/letsencrypt/reverter.py @@ -30,18 +30,18 @@ class Reverter(object): This function should reinstall the users original configuration files for all saves with temporary=True - :raises letsencrypt.errors.LetsEncryptReverterError: when + :raises letsencrypt.errors.ReverterError: when unable to revert config """ if os.path.isdir(self.config.temp_checkpoint_dir): try: self._recover_checkpoint(self.config.temp_checkpoint_dir) - except errors.LetsEncryptReverterError: + except errors.ReverterError: # We have a partial or incomplete recovery logging.fatal("Incomplete or failed recovery for %s", self.config.temp_checkpoint_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to revert temporary config") def rollback_checkpoints(self, rollback=1): @@ -50,7 +50,7 @@ class Reverter(object): :param int rollback: Number of checkpoints to reverse. A str num will be cast to an integer. So "2" is also acceptable. - :raises letsencrypt.errors.LetsEncryptReverterError: If + :raises letsencrypt.errors.ReverterError: If there is a problem with the input or if the function is unable to correctly revert the configuration checkpoints. @@ -59,11 +59,11 @@ class Reverter(object): rollback = int(rollback) except ValueError: logging.error("Rollback argument must be a positive integer") - raise errors.LetsEncryptReverterError("Invalid Input") + raise errors.ReverterError("Invalid Input") # Sanity check input if rollback < 0: logging.error("Rollback argument must be a positive integer") - raise errors.LetsEncryptReverterError("Invalid Input") + raise errors.ReverterError("Invalid Input") backups = os.listdir(self.config.backup_dir) backups.sort() @@ -76,9 +76,9 @@ class Reverter(object): cp_dir = os.path.join(self.config.backup_dir, backups.pop()) try: self._recover_checkpoint(cp_dir) - except errors.LetsEncryptReverterError: + except errors.ReverterError: logging.fatal("Failed to load checkpoint during rollback") - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to load checkpoint during rollback") rollback -= 1 @@ -104,7 +104,7 @@ class Reverter(object): for bkup in backups: float(bkup) except ValueError: - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Invalid directories in {0}".format(self.config.backup_dir)) output = [] @@ -162,7 +162,7 @@ class Reverter(object): :param str save_notes: notes about changes made during the save :raises IOError: If unable to open cp_dir + FILEPATHS file - :raises letsencrypt.errors.LetsEncryptReverterError: If + :raises letsencrypt.errors.ReverterError: If unable to add checkpoint """ @@ -191,7 +191,7 @@ class Reverter(object): logging.error( "Unable to add file %s to checkpoint %s", filename, cp_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to add file {0} to checkpoint " "{1}".format(filename, cp_dir)) idx += 1 @@ -224,7 +224,7 @@ class Reverter(object): :param str cp_dir: checkpoint directory file path - :raises errors.LetsEncryptReverterError: If unable to recover checkpoint + :raises errors.ReverterError: If unable to recover checkpoint """ if os.path.isfile(os.path.join(cp_dir, "FILEPATHS")): @@ -238,7 +238,7 @@ class Reverter(object): except (IOError, OSError): # This file is required in all checkpoints. logging.error("Unable to recover files from %s", cp_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to recover files from %s" % cp_dir) # Remove any newly added files if they exist @@ -248,7 +248,7 @@ class Reverter(object): shutil.rmtree(cp_dir) except OSError: logging.error("Unable to remove directory: %s", cp_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to remove directory: %s" % cp_dir) def _check_tempfile_saves(self, save_files): @@ -256,7 +256,7 @@ class Reverter(object): :param set save_files: Set of files about to be saved. - :raises letsencrypt.errors.LetsEncryptReverterError: + :raises letsencrypt.errors.ReverterError: when save is attempting to overwrite a temporary file. """ @@ -277,7 +277,7 @@ class Reverter(object): # Verify no save_file is in protected_files for filename in protected_files: if filename in save_files: - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Attempting to overwrite challenge " "file - %s" % filename) @@ -292,7 +292,7 @@ class Reverter(object): a temp or permanent save. :param \*files: file paths (str) to be registered - :raises letsencrypt.errors.LetsEncryptReverterError: If + :raises letsencrypt.errors.ReverterError: If call does not contain necessary parameters or if the file creation is unable to be registered. @@ -300,7 +300,7 @@ class Reverter(object): # Make sure some files are provided... as this is an error # Made this mistake in my initial implementation of apache.dvsni.py if not files: - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Forgot to provide files to registration call") if temporary: @@ -322,7 +322,7 @@ class Reverter(object): new_fd.write("{0}{1}".format(path, os.linesep)) except (IOError, OSError): logging.error("Unable to register file creation(s) - %s", files) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to register file creation(s) - {0}".format(files)) finally: if new_fd is not None: @@ -345,12 +345,12 @@ class Reverter(object): if os.path.isdir(self.config.in_progress_dir): try: self._recover_checkpoint(self.config.in_progress_dir) - except errors.LetsEncryptReverterError: + except errors.ReverterError: # We have a partial or incomplete recovery logging.fatal("Incomplete or failed recovery for IN_PROGRESS " "checkpoint - %s", self.config.in_progress_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Incomplete or failed recovery for IN_PROGRESS checkpoint " "- %s" % self.config.in_progress_dir) @@ -362,7 +362,7 @@ class Reverter(object): :returns: Success :rtype: bool - :raises letsencrypt.errors.LetsEncryptReverterError: If + :raises letsencrypt.errors.ReverterError: If all files within file_list cannot be removed """ @@ -386,7 +386,7 @@ class Reverter(object): except (IOError, OSError): logging.fatal( "Unable to remove filepaths contained within %s", file_list) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to remove filepaths contained within " "{0}".format(file_list)) @@ -400,7 +400,7 @@ class Reverter(object): :param str title: Title describing checkpoint - :raises letsencrypt.errors.LetsEncryptReverterError: when the + :raises letsencrypt.errors.ReverterError: when the checkpoint is not able to be finalized. """ @@ -426,7 +426,7 @@ class Reverter(object): shutil.move(changes_since_tmp_path, changes_since_path) except (IOError, OSError): logging.error("Unable to finalize checkpoint - adding title") - raise errors.LetsEncryptReverterError("Unable to add title") + raise errors.ReverterError("Unable to add title") self._timestamp_progress_dir() @@ -451,5 +451,5 @@ class Reverter(object): logging.error( "Unable to finalize checkpoint, %s -> %s", self.config.in_progress_dir, final_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to finalize checkpoint renaming") diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index 402157721..66d359a6b 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -71,7 +71,7 @@ class Revoker(object): authkey.pem).exportKey("PEM") # https://www.dlitz.net/software/pycrypto/api/current/Crypto.PublicKey.RSA-module.html except (IndexError, ValueError, TypeError): - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "Invalid key file specified to revoke_from_key") with open(self.list_path, "rb") as csvfile: @@ -89,7 +89,7 @@ class Revoker(object): # 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.LetsEncryptRevokerError( + raise errors.RevokerError( "%s - backup file is corrupted.") if clean_pem == test_pem: @@ -218,7 +218,7 @@ class Revoker(object): if self.no_confirm or revocation.confirm_revocation(cert): try: self._acme_revoke(cert) - except errors.LetsEncryptClientError: + except errors.Error: # TODO: Improve error handling when networking is set... logging.error( "Unable to revoke cert:%s%s", os.linesep, str(cert)) @@ -250,7 +250,7 @@ class Revoker(object): # If the key file doesn't exist... or is corrupted except (IndexError, ValueError, TypeError): - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "Corrupted backup key file: %s" % cert.backup_key_path) return self.network.revoke(cert=None) # XXX @@ -293,7 +293,7 @@ class Revoker(object): # This should never happen... if idx != len(cert_list): - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "Did not find all cert_list items to remove from LIST") shutil.copy2(list_path2, self.list_path) @@ -398,7 +398,7 @@ class Cert(object): try: self._cert = M2Crypto.X509.load_cert(cert_path) except (IOError, M2Crypto.X509.X509Error): - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "Error loading certificate: %s" % cert_path) self.idx = -1 diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 6e9966a55..03f1958f1 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -73,7 +73,7 @@ class AccountTest(unittest.TestCase): def test_prompts_bad_email(self, mock_from_email, mock_util): from letsencrypt.account import Account - mock_from_email.side_effect = (errors.LetsEncryptClientError, "acc") + mock_from_email.side_effect = (errors.Error, "acc") mock_util().input.return_value = (display_util.OK, self.email) self.assertEqual(Account.from_prompts(self.config), "acc") @@ -102,7 +102,7 @@ class AccountTest(unittest.TestCase): def test_from_email(self): from letsencrypt.account import Account - self.assertRaises(errors.LetsEncryptClientError, + self.assertRaises(errors.Error, Account.from_email, self.config, "not_valid...email") def test_save_from_existing_account(self): @@ -171,7 +171,7 @@ class AccountTest(unittest.TestCase): from letsencrypt.account import Account self.assertRaises( - errors.LetsEncryptClientError, + errors.Error, Account.from_existing_account, self.config, "non-existant@email.org") diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 72fba1d0b..7abc891bc 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -61,7 +61,7 @@ class ChallengeFactoryTest(unittest.TestCase): [mock.Mock(chall="chall", typ="unrecognized")], [messages.STATUS_PENDING]) - self.assertRaises(errors.LetsEncryptClientError, + self.assertRaises(errors.Error, self.handler._challenge_factory, "failure.com", [0]) diff --git a/letsencrypt/tests/continuity_auth_test.py b/letsencrypt/tests/continuity_auth_test.py index 829af736d..95526d265 100644 --- a/letsencrypt/tests/continuity_auth_test.py +++ b/letsencrypt/tests/continuity_auth_test.py @@ -58,7 +58,7 @@ class PerformTest(unittest.TestCase): def test_unexpected(self): self.assertRaises( - errors.LetsEncryptContAuthError, self.auth.perform, [ + errors.ContAuthError, self.auth.perform, [ achallenges.DVSNI(challb=None, domain="0", key="invalid_key")]) def test_chall_pref(self): @@ -91,7 +91,7 @@ class CleanupTest(unittest.TestCase): token = achallenges.RecoveryToken(challb=None, domain="0") unexpected = achallenges.DVSNI(challb=None, domain="0", key="dummy_key") - self.assertRaises(errors.LetsEncryptContAuthError, + self.assertRaises(errors.ContAuthError, self.auth.cleanup, [token, unexpected]) diff --git a/letsencrypt/tests/display/enhancements_test.py b/letsencrypt/tests/display/enhancements_test.py index 54e27aa01..b3a6922d8 100644 --- a/letsencrypt/tests/display/enhancements_test.py +++ b/letsencrypt/tests/display/enhancements_test.py @@ -28,7 +28,7 @@ class AskTest(unittest.TestCase): def test_key_error(self): self.assertRaises( - errors.LetsEncryptClientError, self._call, "unknown_enhancement") + errors.Error, self._call, "unknown_enhancement") class RedirectTest(unittest.TestCase): diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 1ad6968a1..267a930f1 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -45,7 +45,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): def test_existing_wrong_mode_fails(self): self.assertRaises( - errors.LetsEncryptClientError, self._call, self.path, 0o600) + errors.Error, self._call, self.path, 0o600) def test_reraises_os_error(self): with mock.patch.object(os, 'makedirs') as makedirs: diff --git a/letsencrypt/tests/reverter_test.py b/letsencrypt/tests/reverter_test.py index 9da584f58..00d770bcc 100644 --- a/letsencrypt/tests/reverter_test.py +++ b/letsencrypt/tests/reverter_test.py @@ -50,7 +50,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): def test_add_to_checkpoint_copy_failure(self): with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = IOError("bad copy") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint, self.sets[0], "save1") @@ -66,14 +66,14 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.add_to_temp_checkpoint(self.sets[0], "save2") # Raise error self.assertRaises( - errors.LetsEncryptReverterError, self.reverter.add_to_checkpoint, + errors.ReverterError, self.reverter.add_to_checkpoint, self.sets[2], "save3") # Should not cause an error self.reverter.add_to_checkpoint(self.sets[1], "save4") # Check to make sure new files are also checked... self.assertRaises( - errors.LetsEncryptReverterError, + errors.ReverterError, self.reverter.add_to_checkpoint, set([config3]), "invalid save") @@ -120,13 +120,13 @@ class ReverterCheckpointLocalTest(unittest.TestCase): m_open = mock.mock_open() with mock.patch("letsencrypt.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.register_file_creation, True, self.config1) def test_bad_registration(self): # Made this mistake and want to make sure it doesn't happen again... - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.register_file_creation, "filepath") @@ -135,33 +135,33 @@ class ReverterCheckpointLocalTest(unittest.TestCase): # pylint: disable=protected-access self.reverter._recover_checkpoint = mock.MagicMock( - side_effect=errors.LetsEncryptReverterError) - self.assertRaises(errors.LetsEncryptReverterError, + side_effect=errors.ReverterError) + 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.LetsEncryptReverterError("e")) + side_effect=errors.ReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_temp_checkpoint(self.sets[0], "config1 save") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rollback_failure(self): mock_recover = mock.MagicMock( - side_effect=errors.LetsEncryptReverterError("e")) + side_effect=errors.ReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_checkpoint(self.sets[0], "config1 save") self.reverter.finalize_checkpoint("Title") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.rollback_checkpoints, 1) def test_recover_checkpoint_copy_failure(self): @@ -169,7 +169,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = OSError("bad copy") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rm_failure(self): @@ -177,7 +177,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): with mock.patch("letsencrypt.reverter.shutil.rmtree") as mock_rmtree: mock_rmtree.side_effect = OSError("Cannot remove tree") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.revert_temporary_config) @mock.patch("letsencrypt.reverter.logging.warning") @@ -191,7 +191,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): def test_recover_checkpoint_remove_failure(self, mock_remove): self.reverter.register_file_creation(True, self.config1) mock_remove.side_effect = OSError("Can't remove") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.revert_temporary_config) def test_recovery_routine_temp_and_perm(self): @@ -251,13 +251,13 @@ class TestFullCheckpointsReverter(unittest.TestCase): def test_rollback_improper_inputs(self): self.assertRaises( - errors.LetsEncryptReverterError, + errors.ReverterError, self.reverter.rollback_checkpoints, "-1") self.assertRaises( - errors.LetsEncryptReverterError, + errors.ReverterError, self.reverter.rollback_checkpoints, -1000) self.assertRaises( - errors.LetsEncryptReverterError, + errors.ReverterError, self.reverter.rollback_checkpoints, "one") def test_rollback_finalize_checkpoint_valid_inputs(self): @@ -299,7 +299,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_move.side_effect = OSError("cannot move") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @@ -309,7 +309,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_rename.side_effect = OSError - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @@ -347,7 +347,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): # It must just be clean checkpoints os.makedirs(os.path.join(self.config.backup_dir, "in_progress")) - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.view_config_changes) def _setup_three_checkpoints(self): diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index cd86594fd..fa756c4ee 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -80,12 +80,12 @@ class RevokerTest(RevokerBase): @mock.patch("letsencrypt.revoker.Crypto.PublicKey.RSA.importKey") def test_revoke_by_invalid_keys(self, mock_import): mock_import.side_effect = ValueError - self.assertRaises(errors.LetsEncryptRevokerError, + self.assertRaises(errors.RevokerError, self.revoker.revoke_from_key, self.key) mock_import.side_effect = [mock.Mock(), IndexError] - self.assertRaises(errors.LetsEncryptRevokerError, + self.assertRaises(errors.RevokerError, self.revoker.revoke_from_key, self.key) @@ -188,7 +188,7 @@ class RevokerTest(RevokerBase): @mock.patch("letsencrypt.revoker.logging") def test_safe_revoke_acme_fail(self, mock_log, mock_revoke, mock_display): # pylint: disable=protected-access - mock_revoke.side_effect = errors.LetsEncryptClientError + mock_revoke.side_effect = errors.Error mock_display().confirm_revocation.return_value = True self.revoker._safe_revoke(self.certs) @@ -198,7 +198,7 @@ class RevokerTest(RevokerBase): def test_acme_revoke_failure(self, mock_crypto): # pylint: disable=protected-access mock_crypto.side_effect = ValueError - self.assertRaises(errors.LetsEncryptClientError, + self.assertRaises(errors.Error, self.revoker._acme_revoke, self.certs[0]) @@ -215,7 +215,7 @@ class RevokerTest(RevokerBase): new_cert.orig = Cert.PathStatus("false path", "not here") new_cert.orig_key = Cert.PathStatus("false path", "not here") - self.assertRaises(errors.LetsEncryptRevokerError, + self.assertRaises(errors.RevokerError, self.revoker._remove_certs_from_list, [new_cert]) @@ -330,7 +330,7 @@ class CertTest(unittest.TestCase): def test_failed_load(self): from letsencrypt.revoker import Cert - self.assertRaises(errors.LetsEncryptRevokerError, Cert, self.key_path) + self.assertRaises(errors.RevokerError, Cert, self.key_path) def test_no_row(self): self.assertEqual(self.certs[0].get_row(), None) diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 5b0dbdea9..256ada81d 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -554,9 +554,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return self._enhance_func[enhancement]( self.choose_vhost(domain), options) except ValueError: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unsupported enhancement: {}".format(enhancement)) - except errors.LetsEncryptConfiguratorError: + except errors.ConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) def _enable_redirect(self, ssl_vhost, unused_options): @@ -602,7 +602,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return else: logging.info("Unknown redirect exists for this vhost") - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unknown redirect already exists " "in {}".format(general_v.filep)) # Add directives to server @@ -673,7 +673,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Make sure adding the vhost will be safe conflict, host_or_addrs = self._conflicting_host(ssl_vhost) if conflict: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to create a redirection vhost " "- {}".format(host_or_addrs)) @@ -951,7 +951,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :returns: version :rtype: tuple - :raises errors.LetsEncryptConfiguratorError: + :raises errors.ConfiguratorError: Unable to find Apache version """ @@ -962,14 +962,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): stderr=subprocess.PIPE) text = proc.communicate()[0] except (OSError, ValueError): - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to run %s -v" % self.conf('ctl')) regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) matches = regex.findall(text) if len(matches) != 1: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to find Apache version") return tuple([int(i) for i in matches[0].split(".")]) @@ -1079,12 +1079,12 @@ def mod_loaded(module, apache_ctl): except (OSError, ValueError): logging.error( "Error accessing %s for loaded modules!", apache_ctl) - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Error accessing loaded modules") # Small errors that do not impede if proc.returncode != 0: logging.warn("Error in checking loaded module list: %s", stderr) - raise errors.LetsEncryptMisconfigurationError( + raise errors.MisconfigurationError( "Apache is unable to check whether or not the module is " "loaded because Apache is misconfigured.") diff --git a/letsencrypt_apache/parser.py b/letsencrypt_apache/parser.py index 9e6e9efe6..4317df757 100644 --- a/letsencrypt_apache/parser.py +++ b/letsencrypt_apache/parser.py @@ -347,7 +347,7 @@ class ApacheParser(object): if os.path.isfile(os.path.join(self.root, name)): return os.path.join(self.root, name) - raise errors.LetsEncryptNoInstallationError( + raise errors.NoInstallationError( "Could not find configuration root") def _set_user_config_file(self, root): diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/tests/configurator_test.py index c3064eb5b..92cc9762a 100644 --- a/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt_apache/tests/configurator_test.py @@ -198,16 +198,16 @@ class TwoVhost80Test(util.ApacheTest): mock_popen().communicate.return_value = ( "Server Version: Apache (Debian)", "") self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "") self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + errors.ConfiguratorError, self.config.get_version) mock_popen.side_effect = OSError("Can't find program") self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + errors.ConfiguratorError, self.config.get_version) if __name__ == "__main__": diff --git a/letsencrypt_apache/tests/parser_test.py b/letsencrypt_apache/tests/parser_test.py index 06bb20e2a..85cc8abbd 100644 --- a/letsencrypt_apache/tests/parser_test.py +++ b/letsencrypt_apache/tests/parser_test.py @@ -112,7 +112,7 @@ class ApacheParserTest(util.ApacheTest): mock_path.isfile.return_value = False # pylint: disable=protected-access - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.parser._set_locations, self.ssl_options) mock_path.isfile.side_effect = [True, False, False] diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index f74ad0a3a..2a8ac8299 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -128,7 +128,7 @@ class NginxConfigurator(common.Plugin): directives, True) logging.info("Deployed Certificate to VirtualHost %s for %s", vhost.filep, vhost.names) - except errors.LetsEncryptMisconfigurationError: + except errors.MisconfigurationError: logging.warn( "Cannot find a cert or key directive in %s for %s. " "VirtualHost was not modified.", vhost.filep, vhost.names) @@ -315,9 +315,9 @@ class NginxConfigurator(common.Plugin): return self._enhance_func[enhancement]( self.choose_vhost(domain), options) except (KeyError, ValueError): - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unsupported enhancement: {0}".format(enhancement)) - except errors.LetsEncryptConfiguratorError: + except errors.ConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) ###################################### @@ -380,7 +380,7 @@ class NginxConfigurator(common.Plugin): :returns: version :rtype: tuple - :raises errors.LetsEncryptConfiguratorError: + :raises errors.ConfiguratorError: Unable to find Nginx version or version is unsupported """ @@ -391,7 +391,7 @@ class NginxConfigurator(common.Plugin): stderr=subprocess.PIPE) text = proc.communicate()[1] # nginx prints output to stderr except (OSError, ValueError): - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to run %s -V" % self.conf('ctl')) version_regex = re.compile(r"nginx/([0-9\.]*)", re.IGNORECASE) @@ -404,13 +404,13 @@ class NginxConfigurator(common.Plugin): ssl_matches = ssl_regex.findall(text) if not version_matches: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to find Nginx version") if not ssl_matches: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Nginx build is missing SSL module (--with-http_ssl_module).") if not sni_matches: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Nginx build doesn't support SNI") nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) @@ -418,7 +418,7 @@ class NginxConfigurator(common.Plugin): # nginx < 0.8.48 uses machine hostname as default server_name instead of # the empty string if nginx_version < (0, 8, 48): - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Nginx version must be 0.8.48+") return nginx_version diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index f6f82c5cb..1704d92c8 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -79,7 +79,7 @@ class NginxDvsni(common.Dvsni): :param list ll_addrs: list of lists of :class:`letsencrypt_nginx.obj.Addr` to apply - :raises errors.LetsEncryptMisconfigurationError: + :raises errors.MisconfigurationError: Unable to find a suitable HTTP block to include DVSNI hosts. """ @@ -97,7 +97,7 @@ class NginxDvsni(common.Dvsni): included = True break if not included: - raise errors.LetsEncryptMisconfigurationError( + raise errors.MisconfigurationError( 'LetsEncrypt could not find an HTTP block to include DVSNI ' 'challenges in %s.' % root) diff --git a/letsencrypt_nginx/parser.py b/letsencrypt_nginx/parser.py index b25471ef3..6b52dfc15 100644 --- a/letsencrypt_nginx/parser.py +++ b/letsencrypt_nginx/parser.py @@ -195,7 +195,7 @@ class NginxParser(object): if os.path.isfile(os.path.join(self.root, name)): return os.path.join(self.root, name) - raise errors.LetsEncryptNoInstallationError( + raise errors.NoInstallationError( "Could not find configuration root") def filedump(self, ext='tmp'): @@ -486,7 +486,7 @@ def _add_directives(block, directives, replace=False): block[index] = directive changed = True if not changed: - raise errors.LetsEncryptMisconfigurationError( + raise errors.MisconfigurationError( 'LetsEncrypt expected directive for %s in the Nginx ' 'config but did not find it.' % directive[0]) else: diff --git a/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt_nginx/tests/configurator_test.py index 94a0901b5..6b880b14d 100644 --- a/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt_nginx/tests/configurator_test.py @@ -45,7 +45,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual([], self.config.supported_enhancements()) def test_enhance(self): - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.enhance, 'myhost', 'redirect') @@ -218,13 +218,13 @@ class NginxConfiguratorTest(util.NginxTest): " (based on LLVM 3.5svn)", "TLS SNI support enabled", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/1.4.2", "TLS SNI support enabled"])) - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( @@ -232,7 +232,7 @@ class NginxConfiguratorTest(util.NginxTest): "built by clang 6.0 (clang-600.0.56)" " (based on LLVM 3.5svn)", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( @@ -241,12 +241,12 @@ class NginxConfiguratorTest(util.NginxTest): " (based on LLVM 3.5svn)", "TLS SNI support enabled", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen.side_effect = OSError("Can't find program") self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + errors.ConfiguratorError, self.config.get_version) @mock.patch("letsencrypt_nginx.configurator.subprocess.Popen") def test_nginx_restart(self, mock_popen): diff --git a/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt_nginx/tests/dvsni_test.py index 88c25c234..b539c4d78 100644 --- a/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt_nginx/tests/dvsni_test.py @@ -163,7 +163,7 @@ class DvsniPerformTest(util.NginxTest): root = self.sni.configurator.parser.loc["root"] self.sni.configurator.parser.parsed[root] = [['include', 'foo.conf']] # pylint: disable=protected-access - self.assertRaises(errors.LetsEncryptMisconfigurationError, + self.assertRaises(errors.MisconfigurationError, self.sni._mod_config, []) if __name__ == "__main__": diff --git a/letsencrypt_nginx/tests/parser_test.py b/letsencrypt_nginx/tests/parser_test.py index f2e6dfe1e..0af81aefe 100644 --- a/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt_nginx/tests/parser_test.py @@ -5,7 +5,7 @@ import re import shutil import unittest -from letsencrypt.errors import LetsEncryptMisconfigurationError +from letsencrypt import errors from letsencrypt_nginx import nginxparser from letsencrypt_nginx import obj @@ -163,7 +163,7 @@ class NginxParserTest(util.NginxTest): ['listen', '127.0.0.1'], ['server_name', 'foo bar'], ['server_name', 'foo bar']]]]) - self.assertRaises(LetsEncryptMisconfigurationError, + self.assertRaises(errors.MisconfigurationError, nparser.add_server_directives, filep, set(['foo', 'bar']), [['ssl_certificate', 'cert.pem']], True) From 57f67c4109a072a741a3fa925e464ee8abdec3e7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 14:45:28 +0000 Subject: [PATCH 089/109] Rewrap after errors rename, doc fixes. --- acme/client.py | 2 +- letsencrypt/account.py | 3 +- letsencrypt/auth_handler.py | 8 +- letsencrypt/client.py | 19 ++--- letsencrypt/display/enhancements.py | 3 +- letsencrypt/interfaces.py | 8 +- letsencrypt/le_util.py | 2 +- letsencrypt/plugins/disco.py | 3 +- letsencrypt/reverter.py | 17 ++--- letsencrypt/revoker.py | 3 +- letsencrypt/tests/account_test.py | 10 +-- letsencrypt/tests/auth_handler_test.py | 22 +++--- letsencrypt/tests/continuity_auth_test.py | 4 +- .../tests/display/enhancements_test.py | 3 +- letsencrypt/tests/le_util_test.py | 3 +- letsencrypt/tests/reverter_test.py | 76 ++++++++----------- letsencrypt/tests/revoker_test.py | 18 ++--- letsencrypt_apache/configurator.py | 13 ++-- letsencrypt_apache/parser.py | 3 +- letsencrypt_apache/tests/configurator_test.py | 9 +-- letsencrypt_nginx/configurator.py | 11 +-- letsencrypt_nginx/dvsni.py | 2 +- letsencrypt_nginx/tests/configurator_test.py | 21 ++--- letsencrypt_nginx/tests/dvsni_test.py | 4 +- 24 files changed, 109 insertions(+), 158 deletions(-) diff --git a/acme/client.py b/acme/client.py index 4979bce8e..6bb0f5412 100644 --- a/acme/client.py +++ b/acme/client.py @@ -321,7 +321,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes :returns: Challenge Resource with updated body. :rtype: `.ChallengeResource` - :raises errors.UnexpectedUpdate: + :raises .UnexpectedUpdate: """ response = self._post(challb.uri, response) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index e6e46d098..f651bfdb2 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -205,8 +205,7 @@ class Account(object): :param str email: Email address - :raises letsencrypt.errors.Error: If invalid - email address is given. + :raises .errors.Error: If invalid email address is given. """ if not email or cls.safe_email(email): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index a86cdd69d..c3711a244 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -60,7 +60,7 @@ class AuthHandler(object): form of (`completed`, `failed`) :rtype: tuple - :raises AuthorizationError: If unable to retrieve all + :raises .AuthorizationError: If unable to retrieve all authorizations """ @@ -296,8 +296,7 @@ class AuthHandler(object): :class:`letsencrypt.achallenges.Indexed` :rtype: tuple - :raises errors.Error: If Challenge type is not - recognized + :raises .errors.Error: if challenge type is not recognized """ dv_chall = [] @@ -354,8 +353,7 @@ def challb_to_achall(challb, key, domain): else: raise errors.Error( - "Received unsupported challenge of type: %s", - chall.typ) + "Received unsupported challenge of type: %s", chall.typ) def gen_challenge_path(challbs, preferences, combinations): diff --git a/letsencrypt/client.py b/letsencrypt/client.py index e29064a56..279813640 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -147,8 +147,7 @@ class Client(object): logging.warning(msg) raise errors.Error(msg) if self.account.regr is None: - raise errors.Error( - "Please register with the ACME server first.") + raise errors.Error("Please register with the ACME server first.") # Perform Challenges/Get Authorizations authzr = self.auth_handler.get_authorizations(domains) @@ -339,8 +338,8 @@ class Client(object): :param redirect: If traffic should be forwarded from HTTP to HTTPS. :type redirect: bool or None - :raises letsencrypt.errors.Error: if - no installer is specified in the client. + :raises .errors.Error: if no installer is specified in the + client. """ if self.installer is None: @@ -386,8 +385,7 @@ def validate_key_csr(privkey, csr=None): :param csr: CSR :type csr: :class:`letsencrypt.le_util.CSR` - :raises letsencrypt.errors.Error: when - validation fails + :raises .errors.Error: when validation fails """ # TODO: Handle all of these problems appropriately @@ -396,8 +394,7 @@ def validate_key_csr(privkey, csr=None): # Key must be readable and valid. if privkey.pem and not crypto_util.valid_privkey(privkey.pem): - raise errors.Error( - "The provided key is not a valid key") + raise errors.Error("The provided key is not a valid key") if csr: if csr.form == "der": @@ -406,16 +403,14 @@ def validate_key_csr(privkey, csr=None): # If CSR is provided, it must be readable and valid. if csr.data and not crypto_util.valid_csr(csr.data): - raise errors.Error( - "The provided CSR is not a valid CSR") + raise errors.Error("The provided CSR is not a valid CSR") # If both CSR and key are provided, the key must be the same key used # in the CSR. if csr.data and privkey.pem: if not crypto_util.csr_matches_pubkey( csr.data, privkey.pem): - raise errors.Error( - "The key and CSR do not match") + raise errors.Error("The key and CSR do not match") def determine_account(config): diff --git a/letsencrypt/display/enhancements.py b/letsencrypt/display/enhancements.py index 7855b7fba..6d7c78d7d 100644 --- a/letsencrypt/display/enhancements.py +++ b/letsencrypt/display/enhancements.py @@ -21,8 +21,7 @@ def ask(enhancement): :returns: True if feature is desired, False otherwise :rtype: bool - :raises letsencrypt.errors.Error: If - the enhancement provided is not supported. + :raises .errors.Error: if the enhancement provided is not supported """ try: diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 539683d30..d529127e9 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -68,10 +68,10 @@ class IPlugin(zope.interface.Interface): Finish up any additional initialization. - :raises letsencrypt.errors.MisconfigurationError: - when full initialization cannot be completed. Plugin will be - displayed on a list of available plugins. - :raises letsencrypt.errors.NoInstallationError: + :raises .MisconfigurationError: + when full initialization cannot be completed. Plugin will + be displayed on a list of available plugins. + :raises .NoInstallationError: when the necessary programs/files cannot be located. Plugin will NOT be displayed on a list of available plugins. diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 2b0a4a495..e5654a03d 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -19,7 +19,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): :param int mode: Directory mode. :param int uid: Directory owner. - :raises Error: if a directory already exists, + :raises .errors.Error: if a directory already exists, but has wrong permissions or owner :raises OSError: if invalid or inaccessible file names and diff --git a/letsencrypt/plugins/disco.py b/letsencrypt/plugins/disco.py index 229e152e2..6b0d8e4f3 100644 --- a/letsencrypt/plugins/disco.py +++ b/letsencrypt/plugins/disco.py @@ -102,8 +102,7 @@ class PluginEntryPoint(object): @property def misconfigured(self): """Is plugin misconfigured?""" - return isinstance( - self._prepared, errors.MisconfigurationError) + return isinstance(self._prepared, errors.MisconfigurationError) @property def available(self): diff --git a/letsencrypt/reverter.py b/letsencrypt/reverter.py index 2743be97e..72a6c0b67 100644 --- a/letsencrypt/reverter.py +++ b/letsencrypt/reverter.py @@ -30,8 +30,7 @@ class Reverter(object): This function should reinstall the users original configuration files for all saves with temporary=True - :raises letsencrypt.errors.ReverterError: when - unable to revert config + :raises .ReverterError: when unable to revert config """ if os.path.isdir(self.config.temp_checkpoint_dir): @@ -41,8 +40,7 @@ class Reverter(object): # We have a partial or incomplete recovery logging.fatal("Incomplete or failed recovery for %s", self.config.temp_checkpoint_dir) - raise errors.ReverterError( - "Unable to revert temporary config") + raise errors.ReverterError("Unable to revert temporary config") def rollback_checkpoints(self, rollback=1): """Revert 'rollback' number of configuration checkpoints. @@ -50,9 +48,9 @@ class Reverter(object): :param int rollback: Number of checkpoints to reverse. A str num will be cast to an integer. So "2" is also acceptable. - :raises letsencrypt.errors.ReverterError: If - there is a problem with the input or if the function is unable to - correctly revert the configuration checkpoints. + :raises .ReverterError: + if there is a problem with the input or if the function is + unable to correctly revert the configuration checkpoints """ try: @@ -161,9 +159,8 @@ class Reverter(object): :param set save_files: set of files to save :param str save_notes: notes about changes made during the save - :raises IOError: If unable to open cp_dir + FILEPATHS file - :raises letsencrypt.errors.ReverterError: If - unable to add checkpoint + :raises IOError: if unable to open cp_dir + FILEPATHS file + :raises .ReverterError: if unable to add checkpoint """ le_util.make_or_verify_dir( diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index 66d359a6b..a3ea543fb 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -89,8 +89,7 @@ class Revoker(object): # 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.") + raise errors.RevokerError("%s - backup file is corrupted.") if clean_pem == test_pem: certs.append( diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 03f1958f1..6b9fafe31 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -102,8 +102,8 @@ class AccountTest(unittest.TestCase): def test_from_email(self): from letsencrypt.account import Account - self.assertRaises(errors.Error, - Account.from_email, self.config, "not_valid...email") + self.assertRaises( + errors.Error, Account.from_email, self.config, "not_valid...email") def test_save_from_existing_account(self): from letsencrypt.account import Account @@ -170,10 +170,8 @@ class AccountTest(unittest.TestCase): def test_failed_existing_account(self): from letsencrypt.account import Account - self.assertRaises( - errors.Error, - Account.from_existing_account, - self.config, "non-existant@email.org") + self.assertRaises(errors.Error, Account.from_existing_account, + self.config, "non-existant@email.org") class SafeEmailTest(unittest.TestCase): """Test safe_email.""" diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 7abc891bc..24bceb5f8 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -61,8 +61,8 @@ class ChallengeFactoryTest(unittest.TestCase): [mock.Mock(chall="chall", typ="unrecognized")], [messages.STATUS_PENDING]) - self.assertRaises(errors.Error, - self.handler._challenge_factory, "failure.com", [0]) + self.assertRaises( + errors.Error, self.handler._challenge_factory, "failure.com", [0]) class GetAuthorizationsTest(unittest.TestCase): @@ -153,8 +153,8 @@ class GetAuthorizationsTest(unittest.TestCase): gen_dom_authzr, challs=acme_util.CHALLENGES) self.mock_dv_auth.perform.side_effect = errors.AuthorizationError - self.assertRaises(errors.AuthorizationError, - self.handler.get_authorizations, ["0"]) + self.assertRaises( + errors.AuthorizationError, self.handler.get_authorizations, ["0"]) def _validate_all(self, unused_1, unused_2): for dom in self.handler.authzr.keys(): @@ -218,9 +218,9 @@ class PollChallengesTest(unittest.TestCase): @mock.patch("letsencrypt.auth_handler.time") def test_poll_challenges_failure(self, unused_mock_time): self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid - self.assertRaises(errors.AuthorizationError, - self.handler._poll_challenges, - self.chall_update, False) + self.assertRaises( + errors.AuthorizationError, self.handler._poll_challenges, + self.chall_update, False) @mock.patch("letsencrypt.auth_handler.time") def test_unable_to_find_challenge_status(self, unused_mock_time): @@ -229,8 +229,8 @@ class PollChallengesTest(unittest.TestCase): self.chall_update[self.doms[0]].append( challb_to_achall(acme_util.RECOVERY_CONTACT_P, "key", self.doms[0])) self.assertRaises( - errors.AuthorizationError, - self.handler._poll_challenges, self.chall_update, False) + errors.AuthorizationError, self.handler._poll_challenges, + self.chall_update, False) def test_verify_authzr_failure(self): self.assertRaises( @@ -348,8 +348,8 @@ class GenChallengePathTest(unittest.TestCase): prefs = [challenges.DVSNI] combos = ((0, 1),) - self.assertRaises(errors.AuthorizationError, - self._call, challbs, prefs, combos) + self.assertRaises( + errors.AuthorizationError, self._call, challbs, prefs, combos) class MutuallyExclusiveTest(unittest.TestCase): diff --git a/letsencrypt/tests/continuity_auth_test.py b/letsencrypt/tests/continuity_auth_test.py index 95526d265..509dc8bdf 100644 --- a/letsencrypt/tests/continuity_auth_test.py +++ b/letsencrypt/tests/continuity_auth_test.py @@ -91,8 +91,8 @@ class CleanupTest(unittest.TestCase): token = achallenges.RecoveryToken(challb=None, domain="0") unexpected = achallenges.DVSNI(challb=None, domain="0", key="dummy_key") - self.assertRaises(errors.ContAuthError, - self.auth.cleanup, [token, unexpected]) + self.assertRaises( + errors.ContAuthError, self.auth.cleanup, [token, unexpected]) def gen_client_resp(chall): diff --git a/letsencrypt/tests/display/enhancements_test.py b/letsencrypt/tests/display/enhancements_test.py index b3a6922d8..6375316bf 100644 --- a/letsencrypt/tests/display/enhancements_test.py +++ b/letsencrypt/tests/display/enhancements_test.py @@ -27,8 +27,7 @@ class AskTest(unittest.TestCase): self.assertTrue(self._call("redirect")) def test_key_error(self): - self.assertRaises( - errors.Error, self._call, "unknown_enhancement") + self.assertRaises(errors.Error, self._call, "unknown_enhancement") class RedirectTest(unittest.TestCase): diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 267a930f1..7ce619d95 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -44,8 +44,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): self.assertEqual(stat.S_IMODE(os.stat(self.path).st_mode), 0o400) def test_existing_wrong_mode_fails(self): - self.assertRaises( - errors.Error, self._call, self.path, 0o600) + self.assertRaises(errors.Error, self._call, self.path, 0o600) def test_reraises_os_error(self): with mock.patch.object(os, 'makedirs') as makedirs: diff --git a/letsencrypt/tests/reverter_test.py b/letsencrypt/tests/reverter_test.py index 00d770bcc..dda867e4f 100644 --- a/letsencrypt/tests/reverter_test.py +++ b/letsencrypt/tests/reverter_test.py @@ -50,10 +50,9 @@ class ReverterCheckpointLocalTest(unittest.TestCase): def test_add_to_checkpoint_copy_failure(self): with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = IOError("bad copy") - self.assertRaises(errors.ReverterError, - self.reverter.add_to_checkpoint, - self.sets[0], - "save1") + self.assertRaises( + errors.ReverterError, self.reverter.add_to_checkpoint, + self.sets[0], "save1") def test_checkpoint_conflict(self): """Make sure that checkpoint errors are thrown appropriately.""" @@ -65,17 +64,14 @@ class ReverterCheckpointLocalTest(unittest.TestCase): # This shouldn't throw an error self.reverter.add_to_temp_checkpoint(self.sets[0], "save2") # Raise error - self.assertRaises( - errors.ReverterError, self.reverter.add_to_checkpoint, - self.sets[2], "save3") + self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint, + self.sets[2], "save3") # Should not cause an error self.reverter.add_to_checkpoint(self.sets[1], "save4") # Check to make sure new files are also checked... - self.assertRaises( - errors.ReverterError, - self.reverter.add_to_checkpoint, - set([config3]), "invalid save") + self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint, + set([config3]), "invalid save") def test_multiple_saves_and_temp_revert(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") @@ -120,15 +116,15 @@ class ReverterCheckpointLocalTest(unittest.TestCase): m_open = mock.mock_open() with mock.patch("letsencrypt.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") - self.assertRaises(errors.ReverterError, - self.reverter.register_file_creation, - True, self.config1) + self.assertRaises( + errors.ReverterError, self.reverter.register_file_creation, + True, self.config1) def test_bad_registration(self): # Made this mistake and want to make sure it doesn't happen again... - self.assertRaises(errors.ReverterError, - self.reverter.register_file_creation, - "filepath") + self.assertRaises( + errors.ReverterError, self.reverter.register_file_creation, + "filepath") def test_recovery_routine_in_progress_failure(self): self.reverter.add_to_checkpoint(self.sets[0], "perm save") @@ -136,8 +132,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): # pylint: disable=protected-access self.reverter._recover_checkpoint = mock.MagicMock( side_effect=errors.ReverterError) - self.assertRaises(errors.ReverterError, - self.reverter.recovery_routine) + self.assertRaises(errors.ReverterError, self.reverter.recovery_routine) def test_recover_checkpoint_revert_temp_failures(self): # pylint: disable=invalid-name @@ -149,8 +144,8 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.add_to_temp_checkpoint(self.sets[0], "config1 save") - self.assertRaises(errors.ReverterError, - self.reverter.revert_temporary_config) + self.assertRaises( + errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rollback_failure(self): mock_recover = mock.MagicMock( @@ -161,24 +156,24 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "config1 save") self.reverter.finalize_checkpoint("Title") - self.assertRaises(errors.ReverterError, - self.reverter.rollback_checkpoints, 1) + self.assertRaises( + errors.ReverterError, self.reverter.rollback_checkpoints, 1) def test_recover_checkpoint_copy_failure(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = OSError("bad copy") - self.assertRaises(errors.ReverterError, - self.reverter.revert_temporary_config) + self.assertRaises( + errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rm_failure(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "temp save") with mock.patch("letsencrypt.reverter.shutil.rmtree") as mock_rmtree: mock_rmtree.side_effect = OSError("Cannot remove tree") - self.assertRaises(errors.ReverterError, - self.reverter.revert_temporary_config) + self.assertRaises( + errors.ReverterError, self.reverter.revert_temporary_config) @mock.patch("letsencrypt.reverter.logging.warning") def test_recover_checkpoint_missing_new_files(self, mock_warn): @@ -191,8 +186,8 @@ class ReverterCheckpointLocalTest(unittest.TestCase): def test_recover_checkpoint_remove_failure(self, mock_remove): self.reverter.register_file_creation(True, self.config1) mock_remove.side_effect = OSError("Can't remove") - self.assertRaises(errors.ReverterError, - self.reverter.revert_temporary_config) + self.assertRaises( + errors.ReverterError, self.reverter.revert_temporary_config) def test_recovery_routine_temp_and_perm(self): # Register a new perm checkpoint file @@ -251,14 +246,11 @@ class TestFullCheckpointsReverter(unittest.TestCase): def test_rollback_improper_inputs(self): self.assertRaises( - errors.ReverterError, - self.reverter.rollback_checkpoints, "-1") + errors.ReverterError, self.reverter.rollback_checkpoints, "-1") self.assertRaises( - errors.ReverterError, - self.reverter.rollback_checkpoints, -1000) + errors.ReverterError, self.reverter.rollback_checkpoints, -1000) self.assertRaises( - errors.ReverterError, - self.reverter.rollback_checkpoints, "one") + errors.ReverterError, self.reverter.rollback_checkpoints, "one") def test_rollback_finalize_checkpoint_valid_inputs(self): # pylint: disable=invalid-name @@ -299,9 +291,8 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_move.side_effect = OSError("cannot move") - self.assertRaises(errors.ReverterError, - self.reverter.finalize_checkpoint, - "Title") + self.assertRaises( + errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @mock.patch("letsencrypt.reverter.os.rename") def test_finalize_checkpoint_no_rename_directory(self, mock_rename): @@ -309,9 +300,8 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_rename.side_effect = OSError - self.assertRaises(errors.ReverterError, - self.reverter.finalize_checkpoint, - "Title") + self.assertRaises( + errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @mock.patch("letsencrypt.reverter.logging") def test_rollback_too_many(self, mock_logging): @@ -347,8 +337,8 @@ class TestFullCheckpointsReverter(unittest.TestCase): # It must just be clean checkpoints os.makedirs(os.path.join(self.config.backup_dir, "in_progress")) - self.assertRaises(errors.ReverterError, - self.reverter.view_config_changes) + self.assertRaises( + errors.ReverterError, self.reverter.view_config_changes) def _setup_three_checkpoints(self): """Generate some finalized checkpoints.""" diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index fa756c4ee..490ff9f01 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -80,14 +80,12 @@ class RevokerTest(RevokerBase): @mock.patch("letsencrypt.revoker.Crypto.PublicKey.RSA.importKey") def test_revoke_by_invalid_keys(self, mock_import): mock_import.side_effect = ValueError - self.assertRaises(errors.RevokerError, - self.revoker.revoke_from_key, - self.key) + self.assertRaises( + errors.RevokerError, self.revoker.revoke_from_key, self.key) mock_import.side_effect = [mock.Mock(), IndexError] - self.assertRaises(errors.RevokerError, - self.revoker.revoke_from_key, - self.key) + self.assertRaises( + errors.RevokerError, self.revoker.revoke_from_key, self.key) @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") @@ -198,9 +196,8 @@ class RevokerTest(RevokerBase): def test_acme_revoke_failure(self, mock_crypto): # pylint: disable=protected-access mock_crypto.side_effect = ValueError - self.assertRaises(errors.Error, - self.revoker._acme_revoke, - self.certs[0]) + self.assertRaises( + errors.Error, self.revoker._acme_revoke, self.certs[0]) def test_remove_certs_from_list_bad_certs(self): # pylint: disable=protected-access @@ -216,8 +213,7 @@ class RevokerTest(RevokerBase): new_cert.orig_key = Cert.PathStatus("false path", "not here") self.assertRaises(errors.RevokerError, - self.revoker._remove_certs_from_list, - [new_cert]) + self.revoker._remove_certs_from_list, [new_cert]) def _backups_exist(self, row): # pylint: disable=protected-access diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 256ada81d..2a0bf518b 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -674,8 +674,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): conflict, host_or_addrs = self._conflicting_host(ssl_vhost) if conflict: raise errors.ConfiguratorError( - "Unable to create a redirection vhost " - "- {}".format(host_or_addrs)) + "Unable to create a redirection vhost - {}".format( + host_or_addrs)) redirect_addrs = host_or_addrs @@ -951,8 +951,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :returns: version :rtype: tuple - :raises errors.ConfiguratorError: - Unable to find Apache version + :raises .ConfiguratorError: if unable to find Apache version """ try: @@ -969,8 +968,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): matches = regex.findall(text) if len(matches) != 1: - raise errors.ConfiguratorError( - "Unable to find Apache version") + raise errors.ConfiguratorError("Unable to find Apache version") return tuple([int(i) for i in matches[0].split(".")]) @@ -1079,8 +1077,7 @@ def mod_loaded(module, apache_ctl): except (OSError, ValueError): logging.error( "Error accessing %s for loaded modules!", apache_ctl) - raise errors.ConfiguratorError( - "Error accessing loaded modules") + raise errors.ConfiguratorError("Error accessing loaded modules") # Small errors that do not impede if proc.returncode != 0: logging.warn("Error in checking loaded module list: %s", stderr) diff --git a/letsencrypt_apache/parser.py b/letsencrypt_apache/parser.py index 4317df757..5483b96ba 100644 --- a/letsencrypt_apache/parser.py +++ b/letsencrypt_apache/parser.py @@ -347,8 +347,7 @@ class ApacheParser(object): if os.path.isfile(os.path.join(self.root, name)): return os.path.join(self.root, name) - raise errors.NoInstallationError( - "Could not find configuration root") + raise errors.NoInstallationError("Could not find configuration root") def _set_user_config_file(self, root): """Set the appropriate user configuration file diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/tests/configurator_test.py index 92cc9762a..3cdabe0b1 100644 --- a/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt_apache/tests/configurator_test.py @@ -197,17 +197,14 @@ class TwoVhost80Test(util.ApacheTest): mock_popen().communicate.return_value = ( "Server Version: Apache (Debian)", "") - self.assertRaises( - errors.ConfiguratorError, self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "") - self.assertRaises( - errors.ConfiguratorError, self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen.side_effect = OSError("Can't find program") - self.assertRaises( - errors.ConfiguratorError, self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) if __name__ == "__main__": diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index 2a8ac8299..8490e7183 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -380,7 +380,7 @@ class NginxConfigurator(common.Plugin): :returns: version :rtype: tuple - :raises errors.ConfiguratorError: + :raises .ConfiguratorError: Unable to find Nginx version or version is unsupported """ @@ -404,22 +404,19 @@ class NginxConfigurator(common.Plugin): ssl_matches = ssl_regex.findall(text) if not version_matches: - raise errors.ConfiguratorError( - "Unable to find Nginx version") + raise errors.ConfiguratorError("Unable to find Nginx version") if not ssl_matches: raise errors.ConfiguratorError( "Nginx build is missing SSL module (--with-http_ssl_module).") if not sni_matches: - raise errors.ConfiguratorError( - "Nginx build doesn't support SNI") + raise errors.ConfiguratorError("Nginx build doesn't support SNI") nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) # nginx < 0.8.48 uses machine hostname as default server_name instead of # the empty string if nginx_version < (0, 8, 48): - raise errors.ConfiguratorError( - "Nginx version must be 0.8.48+") + raise errors.ConfiguratorError("Nginx version must be 0.8.48+") return nginx_version diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 1704d92c8..b8f9e328a 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -79,7 +79,7 @@ class NginxDvsni(common.Dvsni): :param list ll_addrs: list of lists of :class:`letsencrypt_nginx.obj.Addr` to apply - :raises errors.MisconfigurationError: + :raises .MisconfigurationError: Unable to find a suitable HTTP block to include DVSNI hosts. """ diff --git a/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt_nginx/tests/configurator_test.py index 6b880b14d..4accfa8ea 100644 --- a/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt_nginx/tests/configurator_test.py @@ -45,10 +45,8 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual([], self.config.supported_enhancements()) def test_enhance(self): - self.assertRaises(errors.ConfiguratorError, - self.config.enhance, - 'myhost', - 'redirect') + self.assertRaises( + errors.ConfiguratorError, self.config.enhance, 'myhost', 'redirect') def test_get_chall_pref(self): self.assertEqual([challenges.DVSNI], @@ -218,22 +216,19 @@ class NginxConfiguratorTest(util.NginxTest): " (based on LLVM 3.5svn)", "TLS SNI support enabled", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.ConfiguratorError, - self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/1.4.2", "TLS SNI support enabled"])) - self.assertRaises(errors.ConfiguratorError, - self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/1.4.2", "built by clang 6.0 (clang-600.0.56)" " (based on LLVM 3.5svn)", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.ConfiguratorError, - self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/0.8.1", @@ -241,12 +236,10 @@ class NginxConfiguratorTest(util.NginxTest): " (based on LLVM 3.5svn)", "TLS SNI support enabled", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.ConfiguratorError, - self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen.side_effect = OSError("Can't find program") - self.assertRaises( - errors.ConfiguratorError, self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) @mock.patch("letsencrypt_nginx.configurator.subprocess.Popen") def test_nginx_restart(self, mock_popen): diff --git a/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt_nginx/tests/dvsni_test.py index b539c4d78..ef28e6918 100644 --- a/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt_nginx/tests/dvsni_test.py @@ -163,8 +163,8 @@ class DvsniPerformTest(util.NginxTest): root = self.sni.configurator.parser.loc["root"] self.sni.configurator.parser.parsed[root] = [['include', 'foo.conf']] # pylint: disable=protected-access - self.assertRaises(errors.MisconfigurationError, - self.sni._mod_config, []) + self.assertRaises( + errors.MisconfigurationError, self.sni._mod_config, []) if __name__ == "__main__": unittest.main() # pragma: no cover From a1e750f432ef0c46e6866a68be3309127262680e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 22:45:45 +0000 Subject: [PATCH 090/109] Errors prefix: do not touch CLI. --- letsencrypt/cli.py | 6 +++--- letsencrypt/errors.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index f04727706..3bdf2bfc6 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -41,11 +41,11 @@ def _account_init(args, config): # The way to get the default would be args.email = "" # First try existing account return account.Account.from_existing_account(config, args.email) - except errors.Error: + except errors.LetsEncryptClientError: try: # Try to make an account based on the email address return account.Account.from_email(config, args.email) - except errors.Error: + except errors.LetsEncryptClientError: return None @@ -68,7 +68,7 @@ def _common_run(args, config, acc, authenticator, installer): if acc.regr is None: try: acme.register() - except errors.Error: + except errors.LetsEncryptClientError: sys.exit("Unable to register an account with ACME server") return acme, doms diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index 85f4a69d9..bdcb92164 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -3,6 +3,7 @@ class Error(Exception): """Generic Let's Encrypt client error.""" +LetsEncryptClientError = Error # TODO: blocked by #485 class ReverterError(Error): From 278bd8deb2d33328b7606644da662259132dfde2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 23 Jun 2015 07:34:17 +0000 Subject: [PATCH 091/109] Rename IConfig.csr_dir back to IConfig.cert_dir. This will be used in #504. --- letsencrypt/client.py | 2 +- letsencrypt/configuration.py | 6 +++--- letsencrypt/constants.py | 4 ++-- letsencrypt/interfaces.py | 4 +++- letsencrypt/tests/configuration_test.py | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index e1e30b9be..30bf41975 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -157,7 +157,7 @@ class Client(object): cert_key = crypto_util.init_save_key( self.config.rsa_key_size, self.config.key_dir) csr = crypto_util.init_save_csr( - cert_key, domains, self.config.csr_dir) + cert_key, domains, self.config.cert_dir) # Retrieve certificate certr = self.network.request_issuance( diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 670db0e76..d6b29bd73 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -19,7 +19,7 @@ class NamespaceConfig(object): - `accounts_dir` - `account_keys_dir` - - `csr_dir` + - `cert_dir` - `cert_key_backup` - `in_progress_dir` - `key_dir` @@ -65,8 +65,8 @@ class NamespaceConfig(object): constants.CERT_KEY_BACKUP_DIR, self.server_path) @property - def csr_dir(self): # pylint: disable=missing-docstring - return os.path.join(self.namespace.config_dir, constants.CSR_DIR) + def cert_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.CERT_DIR) @property def in_progress_dir(self): # pylint: disable=missing-docstring diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 152bc224e..5433299fc 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -67,8 +67,8 @@ CERT_KEY_BACKUP_DIR = "keys-certs" """Directory where all certificates and keys are stored (relative to `IConfig.work_dir`). Used for easy revocation.""" -CSR_DIR = "csrs" -"""Directory (relative to `IConfig.config_dir`) where CSRs are saved.""" +CERT_DIR = "certs" +"""See `.IConfig.cert_dir`.""" IN_PROGRESS_DIR = "IN_PROGRESS" """Directory used before a permanent checkpoint is finalized (relative to diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 78ad75f65..d10e29bcd 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -161,7 +161,9 @@ class IConfig(zope.interface.Interface): account_keys_dir = zope.interface.Attribute( "Directory where all account keys are stored.") backup_dir = zope.interface.Attribute("Configuration backups directory.") - csr_dir = zope.interface.Attribute("CSRs storage.") + 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.") diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 38fea140a..d5e9296dd 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -34,7 +34,7 @@ class NamespaceConfigTest(unittest.TestCase): constants.ACCOUNT_KEYS_DIR = 'keys' constants.BACKUP_DIR = 'backups' constants.CERT_KEY_BACKUP_DIR = 'c/' - constants.CSR_DIR = 'csrs' + constants.CERT_DIR = 'certs' constants.IN_PROGRESS_DIR = '../p' constants.KEY_DIR = 'keys' constants.REC_TOKEN_DIR = '/r' @@ -47,7 +47,7 @@ class NamespaceConfigTest(unittest.TestCase): self.config.account_keys_dir, '/tmp/config/acc/acme-server.org:443/new/keys') self.assertEqual(self.config.backup_dir, '/tmp/foo/backups') - self.assertEqual(self.config.csr_dir, '/tmp/config/csrs') + 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') From f1e747ac1ab7db361702f07b408aa033454e0c27 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 23 Jun 2015 07:15:44 +0000 Subject: [PATCH 092/109] Revert CLI changes, blocked by #485. --- letsencrypt/cli.py | 141 +++++++++++++++++--------------------- letsencrypt/constants.py | 8 +++ letsencrypt/interfaces.py | 5 ++ 3 files changed, 74 insertions(+), 80 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 0217598b1..3bdf2bfc6 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -146,21 +146,21 @@ def install(args, config, plugins): return "Installer could not be determined" acme, doms = _common_run( args, config, acc, authenticator=None, installer=installer) - assert args.cert_path is not None # required=True in the subparser + assert args.cert_path is not None acme.deploy_certificate(doms, acc.key.file, args.cert_path, args.chain_path) acme.enhance_config(doms, args.redirect) def revoke(args, unused_config, unused_plugins): """Revoke.""" - if args.cert_path is None and args.key_path is None: - return "At least one of --cert-path or --key-path is required" + if args.rev_cert is None and args.rev_key is None: + return "At least one of --certificate or --key is required" # This depends on the renewal config and cannot be completed yet. zope.component.getUtility(interfaces.IDisplay).notification( "Revocation is not available with the new Boulder server yet.") #client.revoke(args.installer, config, plugins, args.no_confirm, - # args.cert_path, args.key_path) + # args.rev_cert, args.rev_key) def rollback(args, config, plugins): @@ -268,6 +268,34 @@ def create_parser(plugins): "--dvsni-port", type=int, help=config_help("dvsni_port"), default=flag_default("dvsni_port")) + subparsers = 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__) + subparser.set_defaults(func=func) + return subparser + + add_subparser("run", run) + add_subparser("auth", auth) + add_subparser("install", install) + parser_revoke = add_subparser("revoke", revoke) + parser_rollback = add_subparser("rollback", rollback) + add_subparser("config_changes", config_changes) + + parser_plugins = add_subparser("plugins", plugins_cmd) + parser_plugins.add_argument("--init", action="store_true") + parser_plugins.add_argument("--prepare", action="store_true") + parser_plugins.add_argument( + "--authenticators", action="append_const", dest="ifaces", + const=interfaces.IAuthenticator) + parser_plugins.add_argument( + "--installers", action="append_const", dest="ifaces", + const=interfaces.IInstaller) + + parser.add_argument("--configurator") + parser.add_argument("-a", "--authenticator") + parser.add_argument("-i", "--installer") + # positional arg shadows --domains, instead of appending, and # --domains is useful, because it can be stored in config #for subparser in parser_run, parser_auth, parser_install: @@ -286,56 +314,11 @@ def create_parser(plugins): help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") - _paths_parser(parser.add_argument_group("paths")) - # _plugins_parsing should be the last thing to act upon the main - # parser (--help should display plugin-specific options last) - _plugins_parsing(parser, plugins) - - _create_subparsers(parser) - - return parser - - -def _create_subparsers(parser): - subparsers = 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__) - subparser.set_defaults(func=func) - return subparser - - # 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) - add_subparser("auth", auth) - parser_install = add_subparser("install", install) - parser_plugins = add_subparser("plugins", plugins_cmd) - parser_revoke = add_subparser("revoke", revoke) - parser_rollback = add_subparser("rollback", rollback) - add_subparser("config_changes", config_changes) - - parser_install.add_argument( - "--cert-path", required=True, help="Path to a certificate that " - "is going to be installed.") - parser_install.add_argument( - "--chain-path", help="Accompanying path to a certificate chain.") - - parser_plugins.add_argument( - "--init", action="store_true", help="Initialize plugins.") - parser_plugins.add_argument("--prepare", action="store_true", - help="Initialize and prepare plugins.") - parser_plugins.add_argument( - "--authenticators", action="append_const", dest="ifaces", - const=interfaces.IAuthenticator, - help="Limit to authenticator plugins only.") - parser_plugins.add_argument( - "--installers", action="append_const", dest="ifaces", - const=interfaces.IInstaller, help="Limit to installer plugins only.") - parser_revoke.add_argument( - "--cert-path", type=read_file, help="Revoke a specific certificate.") + "--certificate", dest="rev_cert", type=read_file, metavar="CERT_PATH", + help="Revoke a specific certificate.") parser_revoke.add_argument( - "--key-path", type=read_file, + "--key", dest="rev_key", type=read_file, metavar="KEY_PATH", help="Revoke all certs generated by the provided authorized key.") parser_rollback.add_argument( @@ -343,6 +326,16 @@ def _create_subparsers(parser): default=flag_default("rollback_checkpoints"), help="Revert configuration N number of checkpoints.") + _paths_parser(parser.add_argument_group("paths")) + + # TODO: plugin_parser should be called for every detected plugin + for name, plugin_ep in plugins.iteritems(): + plugin_ep.plugin_cls.inject_parser_options( + parser.add_argument_group( + name, description=plugin_ep.description), name) + + return parser + def _paths_parser(parser): add = parser.add_argument @@ -350,38 +343,26 @@ def _paths_parser(parser): help=config_help("config_dir")) add("--work-dir", default=flag_default("work_dir"), help=config_help("work_dir")) + add("--backup-dir", default=flag_default("backup_dir"), + help=config_help("backup_dir")) + add("--key-dir", default=flag_default("key_dir"), + help=config_help("key_dir")) + add("--cert-dir", default=flag_default("certs_dir"), + help=config_help("cert_dir")) + + add("--le-vhost-ext", default="-le-ssl.conf", + help=config_help("le_vhost_ext")) + add("--cert-path", default=flag_default("cert_path"), + help=config_help("cert_path")) + add("--chain-path", default=flag_default("chain_path"), + help=config_help("chain_path")) + + add("--renewer-config-file", default=flag_default("renewer_config_file"), + help=config_help("renewer_config_file")) return parser -def _plugins_parsing(parser, plugins): - plugins_group = parser.add_argument_group( - "plugins", description="Let's Encrypt client supports an extensible " - "plugins architecture. See '%(prog)s plugins' for a list of all " - "available plugins and their names. You can force a particular " - "plugin by setting options provided below. Futher down this help " - "message you will find plugin-specific options (prefixed by " - "--{plugin_name}.") - plugins_group.add_argument( - "-a", "--authenticator", help="Authenticator plugin name.") - plugins_group.add_argument( - "-i", "--installer", help="Installer plugin name.") - plugins_group.add_argument( - "--configurator", help="Name of the plugin that is both " - "an authenticator and an installer. Should not be used together " - "with --authenticator or --installer.") - - # things should not be reorder past/pre this comment: - # plugins_group should be displayed in --help before plugin - # specific groups (so that plugins_group.description makes sense) - - for name, plugin_ep in plugins.iteritems(): - plugin_ep.plugin_cls.inject_parser_options( - parser.add_argument_group( - "plugins: {0}".format(name), - description=plugin_ep.description), name) - - def main(args=sys.argv[1:]): """Command line argument parsing and main script execution.""" # note: arg parser internally handles --help (and exits afterwards) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 5433299fc..df41fbe5b 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -17,6 +17,14 @@ CLI_DEFAULTS = dict( work_dir="/var/lib/letsencrypt", no_verify_ssl=False, dvsni_port=challenges.DVSNI.PORT, + + # TODO: blocked by #485, values ignored + backup_dir="not used", + key_dir="not used", + certs_dir="not used", + cert_path="not used", + chain_path="not used", + renewer_config_file="not used", ) """Defaults for CLI flags and `.IConfig` attributes.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index d10e29bcd..a93716d7d 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -188,6 +188,11 @@ class IConfig(zope.interface.Interface): no_simple_http_tls = zope.interface.Attribute( "Do not use TLS when solving SimpleHTTP challenges.") + # TODO: the following are not used, but blocked by #485 + le_vhost_ext = zope.interface.Attribute("not used") + cert_path = zope.interface.Attribute("not used") + chain_path = zope.interface.Attribute("not used") + class IInstaller(IPlugin): """Generic Let's Encrypt Installer Interface. From 4fb1685b553c519f2ae4b02d95d413b36f9445d8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 23 Jun 2015 15:01:23 +0000 Subject: [PATCH 093/109] Update error codes, add "error" field to ChallengeBody (acme-spec#158). --- acme/messages.py | 15 +++++++++++---- acme/messages_test.py | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/acme/messages.py b/acme/messages.py index c6d15bbf1..31acd6000 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -14,11 +14,15 @@ class Error(jose.JSONObjectWithFields, Exception): """ ERROR_TYPE_NAMESPACE = 'urn:acme:error:' ERROR_TYPE_DESCRIPTIONS = { - 'malformed': 'The request message was malformed', - 'unauthorized': 'The client lacks sufficient authorization', - 'serverInternal': 'The server experienced an internal error', 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', 'badNonce': 'The client sent an unacceptable anti-replay nonce', + 'connection': 'The server could not connect to the client for DV', + 'dnssec': 'The server could not validate a DNSSEC signed domain', + 'malformed': 'The request message was malformed', + 'serverInternal': 'The server experienced an internal error', + 'tls': 'The server experienced a TLS error during DV', + 'unauthorized': 'The client lacks sufficient authorization', + 'unknownHost': 'The server could not resolve a domain name', } typ = jose.Field('type') @@ -220,8 +224,11 @@ class ChallengeBody(ResourceBody): """ __slots__ = ('chall',) uri = jose.Field('uri') - status = jose.Field('status', decoder=Status.from_json) + status = jose.Field('status', decoder=Status.from_json, + omitempty=True, default=STATUS_PENDING) validated = fields.RFC3339Field('validated', omitempty=True) + error = jose.Field('error', decoder=Error.from_json, + omitempty=True, default=None) def to_partial_json(self): jobj = super(ChallengeBody, self).to_partial_json() diff --git a/acme/messages_test.py b/acme/messages_test.py index 9b3c03fbc..dca1cd280 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -198,19 +198,29 @@ class ChallengeBodyTest(unittest.TestCase): self.chall = challenges.DNS(token='foo') from acme.messages import ChallengeBody - from acme.messages import STATUS_VALID - self.status = STATUS_VALID + from acme.messages import Error + from acme.messages import STATUS_INVALID + self.status = STATUS_INVALID + error = Error(typ='serverInternal', + detail='Unable to communicate with DNS server') self.challb = ChallengeBody( - uri='http://challb', chall=self.chall, status=self.status) + uri='http://challb', chall=self.chall, status=self.status, + error=error) self.jobj_to = { 'uri': 'http://challb', 'status': self.status, 'type': 'dns', 'token': 'foo', + 'error': error, } self.jobj_from = self.jobj_to.copy() - self.jobj_from['status'] = 'valid' + self.jobj_from['status'] = 'invalid' + self.jobj_from['error'] = { + 'type': 'urn:acme:error:serverInternal', + 'detail': 'Unable to communicate with DNS server', + } + def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.challb.to_partial_json()) From 457279adb2cc6c886dc743ee4735a7329b841423 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 23 Jun 2015 20:10:20 +0000 Subject: [PATCH 094/109] Add errors.FailedChallenges and update AuthHandler to use it. --- letsencrypt/auth_handler.py | 46 ++++++++++++++++++-------------- letsencrypt/errors.py | 18 +++++++++++++ letsencrypt/tests/errors_test.py | 26 ++++++++++++++++++ 3 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 letsencrypt/tests/errors_test.py diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 50a66c0d0..fb0e71ca1 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -152,6 +152,9 @@ class AuthHandler(object): # Don't send challenges for None and False authenticator responses if resp: self.network.answer_challenge(achall.challb, resp) + # TODO: answer_challenge returns challr, with URI, + # that can be used in _find_updated_challr + # comparisons... active_achalls.append(achall) if achall.domain in chall_update: chall_update[achall.domain].append(achall) @@ -170,23 +173,27 @@ class AuthHandler(object): while dom_to_check and rounds < max_rounds: # TODO: Use retry-after... time.sleep(min_sleep) + all_failed_achalls = set() for domain in dom_to_check: - comp_challs, failed_challs = self._handle_check( + comp_achalls, failed_achalls = self._handle_check( domain, chall_update[domain]) - if len(comp_challs) == len(chall_update[domain]): + if len(comp_achalls) == len(chall_update[domain]): comp_domains.add(domain) - elif not failed_challs: - for chall in comp_challs: - chall_update[domain].remove(chall) + elif not failed_achalls: + for achall, _ in comp_achalls: + chall_update[domain].remove(achall) # We failed some challenges... damage control else: # Right now... just assume a loss and carry on... if best_effort: comp_domains.add(domain) else: - raise errors.AuthorizationError( - "Failed Authorization procedure for %s" % domain) + all_failed_achalls.update( + updated for _, updated in failed_achalls) + + if all_failed_achalls: + raise errors.FailedChallenges(all_failed_achalls) dom_to_check -= comp_domains comp_domains.clear() @@ -204,32 +211,31 @@ class AuthHandler(object): # Note: if the whole authorization is invalid, the individual failed # challenges will be determined here... for achall in achalls: - status = self._get_chall_status(self.authzr[domain], achall) + updated_achall = achall.update(challb=self._find_updated_challb( + self.authzr[domain], achall)) # This does nothing for challenges that have yet to be decided yet. - if status == messages.STATUS_VALID: - completed.append(achall) - elif status == messages.STATUS_INVALID: - failed.append(achall) + if updated_achall.status == messages.STATUS_VALID: + completed.append((achall, updated_achall)) + elif updated_achall.status == messages.STATUS_INVALID: + failed.append((achall, updated_achall)) return completed, failed - def _get_chall_status(self, authzr, achall): # pylint: disable=no-self-use - """Get the status of the challenge. + def _find_updated_challb(self, authzr, achall): # pylint: disable=no-self-use + """Find updated challenge body within Authorization Resource. .. warning:: This assumes only one instance of type of challenge in each challenge resource. - :param authzr: Authorization Resource - :type authzr: :class:`acme.messages.AuthorizationResource` - - :param achall: Annotated challenge for which to get status - :type achall: :class:`letsencrypt.achallenges.AnnotatedChallenge` + :param .AuthorizationResource authzr: Authorization Resource + :param .AnnotatedChallenge achall: Annotated challenge for which + to get status """ for authzr_challb in authzr.body.challenges: if type(authzr_challb.chall) is type(achall.challb.chall): - return authzr_challb.status + return authzr_challb raise errors.AuthorizationError( "Target challenge not found in authorization resource") diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index d9078dbf2..a9c2e5c1f 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -14,6 +14,24 @@ class AuthorizationError(LetsEncryptClientError): """Authorization error.""" +class FailedChallenges(AuthorizationError): + """Failed challenges error. + + :ivar set failed_achalls: Failed `.AnnotatedChallenge` instances. + + """ + def __init__(self, failed_achalls): + assert failed_achalls + self.failed_achalls = failed_achalls + super(FailedChallenges, self).__init__() + + def __str__(self): + return "Failed authorization procedure. {0}".format( + ", ".join( + "{0} ({1}): {2}".format(achall.domain, achall.typ, achall.error) + for achall in self.failed_achalls if achall.error is not None)) + + class LetsEncryptContAuthError(AuthorizationError): """Let's Encrypt Continuity Authenticator error.""" diff --git a/letsencrypt/tests/errors_test.py b/letsencrypt/tests/errors_test.py new file mode 100644 index 000000000..a99d84719 --- /dev/null +++ b/letsencrypt/tests/errors_test.py @@ -0,0 +1,26 @@ +"""Tests for letsencrypt.errors.""" +import unittest + +from acme import messages + +from letsencrypt import achallenges +from letsencrypt.tests import acme_util + + +class FaiiledChallengesTest(unittest.TestCase): + """Tests for letsencrypt.errors.FailedChallenges.""" + + def setUp(self): + from letsencrypt.errors import FailedChallenges + self.error = FailedChallenges(set([achallenges.DNS( + domain="example.com", challb=messages.ChallengeBody( + chall=acme_util.DNS, uri=None, + error=messages.Error(typ="tls", detail="detail")))])) + + def test_str(self): + self.assertTrue(str(self.error).startswith( + "Failed authorization procedure. example.com (dns): tls")) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From 02f3bb4f0532fbe63d1e397cf72d5dfa0e6a1d70 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 23 Jun 2015 14:13:52 -0700 Subject: [PATCH 095/109] Use a completely custom usage mesage for plain --help Keep argparse in place for --help , but try to make that match the customised short help as much as possible. --- letsencrypt/cli.py | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 36055d909..ee95a35c5 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -25,22 +25,30 @@ from letsencrypt import reporter from letsencrypt.display import util as display_util from letsencrypt.display import ops as display_ops - from letsencrypt.plugins import disco as plugins_disco -USAGE = """ +# Argparse's help formatting has a lot of unhelpful peculiarities, so we want +# to replace as much of it as we can... + +# This is the stub to include in help generated by argparse + +SHORT_USAGE = """ letsencrypt [SUBCOMMAND] [options] [domains] The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing -the cert. Major SUBCOMMANDS are: +the cert. """ - (default) Obtain & install a cert in your current webserver - auth Authenticate & obtain cert, but do not install it - install Install a previously obtained cert in a server - revoke Revoke a previously obtained certificate - rollback Rollback server configuration changes made during install - config-changes Show changes made to server config during installation +# This is the short help for letsencrypt --help, where we disable argparse +# altogether +USAGE = SHORT_USAGE + """Major SUBCOMMANDS are: + + (default) everything Obtain & install a cert in your current webserver + auth Authenticate & obtain cert, but do not install it + install Install a previously obtained cert in a server + revoke Revoke a previously obtained certificate + rollback Rollback server configuration changes made during install + config-changes Show changes made to server config during installation Choice of server for authentication/installation: @@ -142,7 +150,7 @@ def run(args, config, plugins): def auth(args, config, plugins): - """Obtain a certificate (no install).""" + """Authenticate & obtain cert, but do not install it""" # XXX: Update for renewer / RenewableCert acc = _account_init(args, config) if acc is None: @@ -167,7 +175,7 @@ def auth(args, config, plugins): def install(args, config, plugins): - """Install cert in server software (no auth).""" + """Install a previously obtained cert in a server""" # XXX: Update for renewer/RenewableCert acc = _account_init(args, config) if acc is None: @@ -184,7 +192,7 @@ def install(args, config, plugins): def revoke(args, unused_config, unused_plugins): - """Revoke.""" + """Revoke a previously obtained certificate""" if args.rev_cert is None and args.rev_key is None: return "At least one of --certificate or --key is required" @@ -196,12 +204,12 @@ def revoke(args, unused_config, unused_plugins): def rollback(args, config, plugins): - """Rollback.""" + """Rollback server configuration changes made during install""" client.rollback(args.installer, args.checkpoints, config, plugins) def config_changes(unused_args, config, unused_plugins): - """View config changes. + """Show changes made to server config during installation View checkpoints and associated configuration changes. @@ -210,7 +218,7 @@ def config_changes(unused_args, config, unused_plugins): def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print - """List plugins.""" + """List server software plugins""" logging.debug("Expected interfaces: %s", args.ifaces) ifaces = [] if args.ifaces is None else args.ifaces @@ -285,16 +293,22 @@ class HelpfulArgumentParser(object): plugin_names = [name for name, _p in plugins.iteritems()] self.help_topics = HELP_TOPICS + plugin_names self.parser = configargparse.ArgParser( - description=__doc__, + usage=SHORT_USAGE, formatter_class=argparse.ArgumentDefaultsHelpFormatter, args_for_setting_config_path=["-c", "--config"], default_config_files=flag_default("config_files")) + + self.parser._add_config_file_help = False self.silent_parser = SilentParser(self.parser) h1 = self.prescan_for_flag("-h", self.help_topics) h2 = self.prescan_for_flag("--help", self.help_topics) assert max(True, "a") == "a", "Gravity changed direction" help_arg = max(h1, h2) + if help_arg == True: + # just --help with no topic; avoid argparse altogether + print USAGE + sys.exit(0) self.visible_topics = self.determine_help_topics(help_arg) #print self.visible_topics self.groups = {} # elements are added by .add_group() From 079eb93e533093d9500ab0cf90002ab1490fb34e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 23 Jun 2015 15:10:42 -0700 Subject: [PATCH 096/109] Satisfy the lintmonster --- letsencrypt/cli.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index ee95a35c5..bdc287370 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -298,17 +298,18 @@ class HelpfulArgumentParser(object): args_for_setting_config_path=["-c", "--config"], default_config_files=flag_default("config_files")) - self.parser._add_config_file_help = False + # This is the only way to turn off overly verbose config flag documentation + self.parser._add_config_file_help = False # pylint: disable=protected-access self.silent_parser = SilentParser(self.parser) - h1 = self.prescan_for_flag("-h", self.help_topics) - h2 = self.prescan_for_flag("--help", self.help_topics) + help1 = self.prescan_for_flag("-h", self.help_topics) + help2 = self.prescan_for_flag("--help", self.help_topics) assert max(True, "a") == "a", "Gravity changed direction" - help_arg = max(h1, h2) + help_arg = max(help1, help2) if help_arg == True: - # just --help with no topic; avoid argparse altogether - print USAGE - sys.exit(0) + # just --help with no topic; avoid argparse altogether + print USAGE + sys.exit(0) self.visible_topics = self.determine_help_topics(help_arg) #print self.visible_topics self.groups = {} # elements are added by .add_group() From fc4c5991877b87d24305503cbfb618c9535208d1 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 23 Jun 2015 18:38:24 -0400 Subject: [PATCH 097/109] Remove preview from github and travis --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 40c054fe3..784e06436 100644 --- a/README.rst +++ b/README.rst @@ -41,8 +41,8 @@ server automatically!:: **Encrypt ALL the things!** -.. |build-status| image:: https://travis-ci.org/letsencrypt/lets-encrypt-preview.svg?branch=master - :target: https://travis-ci.org/letsencrypt/lets-encrypt-preview +.. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master + :target: https://travis-ci.org/letsencrypt/letsencrypt :alt: Travis CI status .. |coverage| image:: https://coveralls.io/repos/letsencrypt/lets-encrypt-preview/badge.svg?branch=master @@ -100,7 +100,7 @@ Links Documentation: https://letsencrypt.readthedocs.org -Software project: https://github.com/letsencrypt/lets-encrypt-preview +Software project: https://github.com/letsencrypt/letsencrypt Notes for developers: CONTRIBUTING.md_ @@ -113,4 +113,4 @@ email to client-dev+subscribe@letsencrypt.org) .. _Freenode: https://freenode.net .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev -.. _CONTRIBUTING.md: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.md +.. _CONTRIBUTING.md: https://github.com/letsencrypt/letsencrypt/blob/master/CONTRIBUTING.md From a1f025980a51e6869ab2cef15bbf6bd7788e0ff0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 23 Jun 2015 16:38:39 -0600 Subject: [PATCH 098/109] minor update for raspbian --- bootstrap/_deb_common.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 653daca53..6bb8479c8 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -9,7 +9,11 @@ # - 6.0.10 "squeeze" (x64) # - 7.8 "wheezy" (x64) # - 8.0 "jessie" (x64) +# - Raspbian: +# - 7.8 (armhf) +apt-get update +apt-get install -y lsb-release # virtualenv binary can be found in different packages depending on # distro version (#346) @@ -43,7 +47,6 @@ fi # #276, https://github.com/martinpaljak/M2Crypto/issues/62, # M2Crypto setup.py:add_multiarch_paths -apt-get update apt-get install -y --no-install-recommends \ git-core \ python \ From 7fe5b8233bee880a2b143135fd7632f797f94905 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 23 Jun 2015 17:38:02 -0700 Subject: [PATCH 099/109] Retry, with sanity. --- letsencrypt/le_util.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index f8c457c37..0f8207b7a 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -32,14 +32,8 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): except OSError as exception: if exception.errno == errno.EEXIST: if not check_permissions(directory, mode, uid): -<<<<<<< HEAD - raise errors.LetsEncryptClientError( - "%s exists, this client can't access it" % directory) -======= raise errors.Error( - "%s exists, but does not have the proper " - "permissions or owner" % directory) ->>>>>>> letsencrypt/master + "%s exists, this client can't access it" % directory) else: raise From 15258cc50a6f8694eab0710d4e5a6fad15b657c0 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 24 Jun 2015 04:53:11 +0000 Subject: [PATCH 100/109] Update references after repo rename. https://github.com/letsencrypt/letsencrypt/issues/505 --- CHANGES.rst | 2 +- Dockerfile | 2 +- LICENSE.txt | 2 +- README.rst | 8 ++++---- docs/contributing.rst | 2 +- docs/pkgs.rst | 2 +- docs/using.rst | 10 +++++----- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 741d9bc7c..3ed13041b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ ChangeLog Please note: the change log will only get updated after first release - for now please use the -`commit log `_. +`commit log `_. Release 0.1.0 (not released yet) diff --git a/Dockerfile b/Dockerfile index 78aa7a75b..479aa4e85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# https://github.com/letsencrypt/lets-encrypt-preview/pull/431#issuecomment-103659297 +# https://github.com/letsencrypt/letsencrypt/pull/431#issuecomment-103659297 # it is more likely developers will already have ubuntu:trusty rather # than e.g. debian:jessie and image size differences are negligible FROM ubuntu:trusty diff --git a/LICENSE.txt b/LICENSE.txt index d3c19bbd1..5a9f6fa55 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Let's Encrypt Preview: +Let's Encrypt: Copyright (c) Internet Security Research Group Licensed Apache Version 2.0 diff --git a/README.rst b/README.rst index 784e06436..7c98999e8 100644 --- a/README.rst +++ b/README.rst @@ -45,16 +45,16 @@ server automatically!:: :target: https://travis-ci.org/letsencrypt/letsencrypt :alt: Travis CI status -.. |coverage| image:: https://coveralls.io/repos/letsencrypt/lets-encrypt-preview/badge.svg?branch=master - :target: https://coveralls.io/r/letsencrypt/lets-encrypt-preview +.. |coverage| image:: https://coveralls.io/repos/letsencrypt/letsencrypt/badge.svg?branch=master + :target: https://coveralls.io/r/letsencrypt/letsencrypt :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ :target: https://readthedocs.org/projects/letsencrypt/ :alt: Documentation status -.. |container| image:: https://quay.io/repository/letsencrypt/lets-encrypt-preview/status - :target: https://quay.io/repository/letsencrypt/lets-encrypt-preview +.. |container| image:: https://quay.io/repository/letsencrypt/letsencrypt/status + :target: https://quay.io/repository/letsencrypt/letsencrypt :alt: Docker Repository on Quay.io .. _`installation instructions`: diff --git a/docs/contributing.rst b/docs/contributing.rst index 804cec95c..05a6875fe 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -97,7 +97,7 @@ Configurators may implement just one of those). There are also `~letsencrypt.interfaces.IDisplay` plugins, which implement bindings to alternative UI libraries. -.. _interfaces.py: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/letsencrypt/interfaces.py +.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py Authenticators diff --git a/docs/pkgs.rst b/docs/pkgs.rst index 8119ffc7e..2e1b18dfb 100644 --- a/docs/pkgs.rst +++ b/docs/pkgs.rst @@ -6,7 +6,7 @@ Packages described in `#358`_. For the time being those packages are bundled together into a single repo, and single documentation. -.. _`#358`: https://github.com/letsencrypt/lets-encrypt-preview/issues/358 +.. _`#358`: https://github.com/letsencrypt/letsencrypt/issues/358 .. toctree:: :glob: diff --git a/docs/using.rst b/docs/using.rst index 96eb62b05..951c991d2 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -14,7 +14,7 @@ server that the domain your requesting a cert for resolves to, sudo docker run -it --rm -p 443:443 --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ - quay.io/letsencrypt/lets-encrypt-preview:latest + quay.io/letsencrypt/letsencrypt:latest and follow the instructions. Your new cert will be available in ``/etc/letsencrypt/certs``. @@ -30,8 +30,8 @@ Please `install Git`_ and run the following commands: .. code-block:: shell - git clone https://github.com/letsencrypt/lets-encrypt-preview - cd lets-encrypt-preview + git clone https://github.com/letsencrypt/letsencrypt + cd letsencrypt Alternatively you could `download the ZIP archive`_ and extract the snapshot of our repository, but it's strongly recommended to use the @@ -39,7 +39,7 @@ above method instead. .. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git .. _`download the ZIP archive`: - https://github.com/letsencrypt/lets-encrypt-preview/archive/master.zip + https://github.com/letsencrypt/letsencrypt/archive/master.zip Prerequisites @@ -76,7 +76,7 @@ For squeeze you will need to: - Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``. -.. _`#280`: https://github.com/letsencrypt/lets-encrypt-preview/issues/280 +.. _`#280`: https://github.com/letsencrypt/letsencrypt/issues/280 Mac OSX From 512e02c837347f7ef821925ee61452645c97adaf Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Jun 2015 11:46:39 -0700 Subject: [PATCH 101/109] Added reporter messages for failed challenges. --- letsencrypt/auth_handler.py | 87 +++++++++++++++++++++++++++++++++++++ letsencrypt/reporter.py | 23 +++++++--- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index d7d590878..502b0b76d 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -3,12 +3,15 @@ import itertools import logging import time +import zope.component + from acme import challenges from acme import messages from letsencrypt import achallenges from letsencrypt import constants from letsencrypt import errors +from letsencrypt import interfaces class AuthHandler(object): @@ -193,6 +196,7 @@ class AuthHandler(object): updated for _, updated in failed_achalls) if all_failed_achalls: + _report_failed_challs(all_failed_achalls) raise errors.FailedChallenges(all_failed_achalls) dom_to_check -= comp_domains @@ -480,3 +484,86 @@ def is_preferred(offered_challb, satisfied, different=True): return False return True + + +_ERROR_HELP_COMMON = ( + 'To fix these errors, please make sure that your domain name was entered ' + 'correctly and the DNS A/AAAA record(s) for that domain contains the ' + 'right IP address.' +) + + +_ERROR_HELP = { + 'connection' : + _ERROR_HELP_COMMON + ' Additionally, please check that your computer ' + 'has publicly routable IP address and no firewalls are preventing the ' + 'server from communicating with the client.', + 'dnssec' : + _ERROR_HELP_COMMON + ' Additionally, if you have DNSSEC enabled for ' + 'your domain, please ensure the signature is valid.', + 'malformed' : + 'To fix these errors, please make sure that you did not provide any ' + 'invalid information to the client and try running Let\'s Encrypt ' + 'again.', + 'serverInternal' : + 'Unfortunately, an error on the ACME server prevented you from completing ' + 'authorization. Please try again later.', + 'tls' : + _ERROR_HELP_COMMON + ' Additionally, please check that you have an up ' + 'to date TLS configuration that allows the server to communicate with ' + 'the Let\'s Encrypt client.', + 'unauthorized' : _ERROR_HELP_COMMON, + 'unknownHost' : + 'To fix these errors, please make sure that your domain name was ' + 'entered correctly.', +} + + +def _report_failed_challs(failed_achalls): + """Notifies the user about failed challenges. + + :param set failed_achalls: A set of failed + :class:`letsencrypt.achallenges.AnnotatedChallenge`. + + """ + problems = dict() + for achall in failed_achalls: + if achall.error: + problems.setdefault(achall.error.typ, []).append(achall) + + reporter = zope.component.getUtility(interfaces.IReporter) + for achalls in problems.itervalues(): + reporter.add_message(_generate_failed_chall_msg(achalls), 1, True) + + +def _generate_failed_chall_msg(failed_achalls): + """Creates a user friendly error message about failed challenges. + + :param list failed_achalls: A list of failed + :class:`letsencrypt.achallenges.AnnotatedChallenge` with the same error + type. + + :returns: A formatted error message for the client. + :rtype: str + + """ + typ = failed_achalls[0].error.typ + msg = [ + 'The following \'{0}\' errors were reported by the server:'.format(typ) + ] + + problems = dict() + for achall in failed_achalls: + problems.setdefault(achall.error.description, []).append(achall.domain) + for problem in problems: + domains = problems[problem] + domains.sort() + msg.append('\n\nDomains: ') + msg.append(', '.join(domains)) + msg.append('\nError: {0}'.format(problem)) + + if typ in _ERROR_HELP: + msg.append('\n\n') + msg.append(_ERROR_HELP[typ]) + + return "".join(msg) diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 045c1befa..3045a7e19 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -46,9 +46,10 @@ class Reporter(object): printed if the program exits abnormally. """ - assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY - self.messages.put(self._msg_type(priority, msg, on_crash)) - logging.info("Reporting to user: %s", msg) + if msg: + assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY + self.messages.put(self._msg_type(priority, msg, on_crash)) + logging.info("Reporting to user: %s", msg) def atexit_print_messages(self, pid=os.getpid()): """Function to be registered with atexit to print messages. @@ -66,7 +67,8 @@ class Reporter(object): If there is an unhandled exception, only messages for which ``on_crash`` is ``True`` are printed. -""" + + """ bold_on = False if not self.messages.empty(): no_exception = sys.exc_info()[0] is None @@ -74,14 +76,21 @@ class Reporter(object): if bold_on: print self._BOLD print 'IMPORTANT NOTES:' - wrapper = textwrap.TextWrapper(initial_indent=' - ', - subsequent_indent=(' ' * 3)) + first_wrapper = textwrap.TextWrapper( + initial_indent=' - ', subsequent_indent=(' ' * 3)) + next_wrapper = textwrap.TextWrapper( + initial_indent=first_wrapper.subsequent_indent, + subsequent_indent=first_wrapper.subsequent_indent) while not self.messages.empty(): 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) bold_on = False - print wrapper.fill(msg.text) + lines = msg.text.splitlines() + print first_wrapper.fill(lines[0]) + if len(lines) > 1: + print "\n".join( + next_wrapper.fill(line) for line in lines[1:]) if bold_on: sys.stdout.write(self._RESET) From 8f760cf828c0b1038313ecdc3483014f2a0107a9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Jun 2015 11:51:50 -0700 Subject: [PATCH 102/109] Cleaned up multiline statements --- letsencrypt/auth_handler.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 502b0b76d..4a685439f 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -489,8 +489,7 @@ def is_preferred(offered_challb, satisfied, _ERROR_HELP_COMMON = ( 'To fix these errors, please make sure that your domain name was entered ' 'correctly and the DNS A/AAAA record(s) for that domain contains the ' - 'right IP address.' -) + 'right IP address.') _ERROR_HELP = { @@ -515,8 +514,7 @@ _ERROR_HELP = { 'unauthorized' : _ERROR_HELP_COMMON, 'unknownHost' : 'To fix these errors, please make sure that your domain name was ' - 'entered correctly.', -} + 'entered correctly.',} def _report_failed_challs(failed_achalls): @@ -533,7 +531,8 @@ 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), 1, True) + reporter.add_message( + _generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY, True) def _generate_failed_chall_msg(failed_achalls): @@ -549,8 +548,7 @@ def _generate_failed_chall_msg(failed_achalls): """ typ = failed_achalls[0].error.typ msg = [ - 'The following \'{0}\' errors were reported by the server:'.format(typ) - ] + "The following '{0}' errors were reported by the server:".format(typ)] problems = dict() for achall in failed_achalls: From fba2de706e2591dfcb79e9963707082f968ad2a2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 24 Jun 2015 13:29:26 -0600 Subject: [PATCH 103/109] move lsb install into newer with --no-install-recommends --- bootstrap/_deb_common.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 6bb8479c8..a4dff6b1c 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -12,12 +12,11 @@ # - Raspbian: # - 7.8 (armhf) -apt-get update -apt-get install -y lsb-release # virtualenv binary can be found in different packages depending on # distro version (#346) newer () { + apt-get install -y lsb-release --no-install-recommends distro=$(lsb_release -si) # 6.0.10 => 60, 14.04 => 1404 # TODO: in sid version==unstable @@ -33,6 +32,8 @@ newer () { fi } +apt-get update + # you can force newer if lsb_release is not available (e.g. Docker # debian:jessie base image) if [ "$1" = "newer" ] || newer From 06d7e51f22c182abb0a453ba422c185b92e9c4f8 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 24 Jun 2015 14:38:30 -0700 Subject: [PATCH 104/109] formatting and documentation --- .pylintrc | 7 ++- letsencrypt/cli.py | 104 ++++++++++++++++++++++++++++++--------------- 2 files changed, 72 insertions(+), 39 deletions(-) diff --git a/.pylintrc b/.pylintrc index 825699036..5302133ad 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 +disable=fixme,locally-disabled,abstract-class-not-used # abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1) @@ -101,7 +101,7 @@ function-rgx=[a-z_][a-z0-9_]{2,40}$ function-name-hint=[a-z_][a-z0-9_]{2,40}$ # Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{1,30}$ +variable-rgx=[a-z_][a-z0-9_]{2,30}$ # Naming hint for variable names variable-name-hint=[a-z_][a-z0-9_]{2,30}$ @@ -228,8 +228,7 @@ max-module-lines=1250 indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. -# This does something silly/broken... -#indent-after-paren=4 +indent-after-paren=4 [TYPECHECK] diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index bdc287370..b2ecd4887 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -150,7 +150,7 @@ def run(args, config, plugins): def auth(args, config, plugins): - """Authenticate & obtain cert, but do not install it""" + """Authenticate & obtain cert, but do not install it.""" # XXX: Update for renewer / RenewableCert acc = _account_init(args, config) if acc is None: @@ -169,13 +169,13 @@ def auth(args, config, plugins): # TODO: Handle errors from _common_run? acme, doms = _common_run( args, config, acc, authenticator=authenticator, installer=installer) - if not acme.obtain_and_enroll_certificate(doms, authenticator, installer, - plugins): + if not acme.obtain_and_enroll_certificate( + doms, authenticator, installer, plugins): return "Certificate could not be obtained" def install(args, config, plugins): - """Install a previously obtained cert in a server""" + """Install a previously obtained cert in a server.""" # XXX: Update for renewer/RenewableCert acc = _account_init(args, config) if acc is None: @@ -192,7 +192,7 @@ def install(args, config, plugins): def revoke(args, unused_config, unused_plugins): - """Revoke a previously obtained certificate""" + """Revoke a previously obtained certificate.""" if args.rev_cert is None and args.rev_key is None: return "At least one of --certificate or --key is required" @@ -204,7 +204,7 @@ def revoke(args, unused_config, unused_plugins): def rollback(args, config, plugins): - """Rollback server configuration changes made during install""" + """Rollback server configuration changes made during install.""" client.rollback(args.installer, args.checkpoints, config, plugins) @@ -218,7 +218,7 @@ def config_changes(unused_args, config, unused_plugins): def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print - """List server software plugins""" + """List server software plugins.""" logging.debug("Expected interfaces: %s", args.ifaces) ifaces = [] if args.ifaces is None else args.ifaces @@ -264,6 +264,7 @@ def flag_default(name): """Default value for CLI flag.""" return constants.CLI_DEFAULTS[name] + def config_help(name, hidden=False): """Help message for `.IConfig` attribute.""" if hidden: @@ -271,10 +272,15 @@ def config_help(name, hidden=False): else: return interfaces.IConfig[name].__doc__ -class SilentParser(object): - """An a mini parser wrapper that doesn't print help for its - arguments... this one is just needed to the use of callbacks to define - arguments within plugins""" + +class SilentParser(object): # pylint: disable=too-few-public-methods + """Silent wrapper around argparse. + + A mini parser wrapper that doesn't print help for its + arguments. This is needed for the use of callbacks to define + arguments within plugins. + + """ def __init__(self, parser): self.parser = parser def add_argument(self, *args, **kwargs): @@ -282,12 +288,18 @@ class SilentParser(object): kwargs["help"] = argparse.SUPPRESS self.parser.add_argument(*args, **kwargs) -HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] -class HelpfulArgumentParser(object): - """This class wraps argparse, adding the ability to make --help less - verbose, and request help on specific subcategories at a time, eg - 'letsencrypt --help security' for security options.""" +HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + + +class HelpfulArgumentParser(object): + """Argparse Wrapper. + + This class wraps argparse, adding the ability to make --help less + verbose, and request help on specific subcategories at a time, eg + 'letsencrypt --help security' for security options. + + """ def __init__(self, args, plugins): self.args = args plugin_names = [name for name, _p in plugins.iteritems()] @@ -316,11 +328,15 @@ class HelpfulArgumentParser(object): self.add_plugin_args(plugins) def prescan_for_flag(self, flag, possible_arguments): - """check for a flag, which accepts a fixed set of possible arguments, in + """Checks cli input for flags. + + Check for a flag, which accepts a fixed set of possible arguments, in the command line; we will use this information to configure argparse's help correctly. Return the flag's argument, if it has one that matches the sequence @possible_arguments; otherwise return whether the flag is - present""" + present. + + """ if flag not in self.args: return False pos = self.args.index(flag) @@ -333,10 +349,12 @@ class HelpfulArgumentParser(object): return True def add(self, topic, *args, **kwargs): - """Add a new command line argument. @topic is required, to indicate - which part of the help will document it, but can be None for `always - documented'.""" + """Add a new command line argument. + @topic is required, to indicate which part of the help will document + it, but can be None for `always documented'. + + """ if topic and self.visible_topics[topic]: group = self.groups[topic] group.add_argument(*args, **kwargs) @@ -345,10 +363,14 @@ class HelpfulArgumentParser(object): self.parser.add_argument(*args, **kwargs) def add_group(self, topic, **kwargs): - """This has to be called once for every topic; but we leave those calls + """ + + This has to be called once for every topic; but we leave those calls next to the argument definitions for clarity. Return something arguments can be added to if necessary, either the parser or an argument - group.""" + group. + + """ if self.visible_topics[topic]: #print "Adding visible group " + topic group = self.parser.add_argument_group(topic, **kwargs) @@ -359,8 +381,12 @@ class HelpfulArgumentParser(object): return self.silent_parser def add_plugin_args(self, plugins): - """Let each of the plugins add its own command line arguments, which - may or may not be displayed as help topics.""" + """ + + Let each of the plugins add its own command line arguments, which + may or may not be displayed as help topics. + + """ # TODO: plugin_parser should be called for every detected plugin for name, plugin_ep in plugins.iteritems(): parser_or_group = self.add_group(name, description=plugin_ep.description) @@ -368,9 +394,14 @@ class HelpfulArgumentParser(object): plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name) def determine_help_topics(self, chosen_topic): - """The user may have requested help on a topic, return a dict of which - topics to dislpay. @chosen_topic has prescan_for_flag's return type""" + """ + The user may have requested help on a topic, return a dict of which + topics to display. @chosen_topic has prescan_for_flag's return type + + :returns: dict + + """ # topics maps each topic to whether it should be documented by # argparse on the command line if chosen_topic == "all": @@ -392,7 +423,8 @@ def create_parser(plugins, args): "e.g. -vvv.") # --help is automatically provided by argparse - helpful.add_group("automation", + helpful.add_group( + "automation", description="Arguments for automating execution & other tweaks") helpful.add( "automation", "--version", action="version", @@ -423,9 +455,10 @@ def create_parser(plugins, args): help=config_help("dvsni_port")) helpful.add("testing", "--no-simple-http-tls", action="store_true", - help=config_help("no_simple_http_tls")) + help=config_help("no_simple_http_tls")) 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__) @@ -460,16 +493,17 @@ def create_parser(plugins, args): helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append") helpful.add(None, "-k", "--accountkey", type=read_file, - help="Path to the account key file") + help="Path to the account key file") helpful.add(None, "-m", "--email", help=config_help("email")) helpful.add_group( "security", description="Security parameters & server settings") - helpful.add("security", "-B", "--rsa-key-size", type=int, metavar="N", - default=flag_default("rsa_key_size"), - help=config_help("rsa_key_size")) + helpful.add( + "security", "-B", "--rsa-key-size", type=int, metavar="N", + default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) # TODO: resolve - assumes binary logic while client.py assumes ternary. - helpful.add("security", "-r", "--redirect", action="store_true", + helpful.add( + "security", "-r", "--redirect", action="store_true", help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") @@ -487,7 +521,6 @@ def create_parser(plugins, args): _paths_parser(helpful) - return helpful.parser @@ -515,6 +548,7 @@ def _paths_parser(helpful): add("paths", "-s", "--server", default=flag_default("server"), help=config_help("server")) + def main(args=sys.argv[1:]): """Command line argument parsing and main script execution.""" # note: arg parser internally handles --help (and exits afterwards) From 9637142c4c5a19dad1722aa0274100a150a9d8a3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Jun 2015 15:27:34 -0700 Subject: [PATCH 105/109] Added auth_handler tests --- letsencrypt/auth_handler.py | 12 ++---- letsencrypt/tests/auth_handler_test.py | 51 +++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 4a685439f..019cb07dc 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -512,9 +512,7 @@ _ERROR_HELP = { 'to date TLS configuration that allows the server to communicate with ' 'the Let\'s Encrypt client.', 'unauthorized' : _ERROR_HELP_COMMON, - 'unknownHost' : - 'To fix these errors, please make sure that your domain name was ' - 'entered correctly.',} + 'unknownHost' : _ERROR_HELP_COMMON,} def _report_failed_challs(failed_achalls): @@ -552,16 +550,14 @@ def _generate_failed_chall_msg(failed_achalls): problems = dict() for achall in failed_achalls: - problems.setdefault(achall.error.description, []).append(achall.domain) + problems.setdefault(achall.error.description, set()).add(achall.domain) for problem in problems: - domains = problems[problem] - domains.sort() msg.append('\n\nDomains: ') - msg.append(', '.join(domains)) + msg.append(', '.join(sorted(problems[problem]))) msg.append('\nError: {0}'.format(problem)) if typ in _ERROR_HELP: msg.append('\n\n') msg.append(_ERROR_HELP[typ]) - return "".join(msg) + return ''.join(msg) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 24bceb5f8..15ce6a490 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -216,7 +216,8 @@ class PollChallengesTest(unittest.TestCase): self.assertEqual(authzr.body.status, messages.STATUS_PENDING) @mock.patch("letsencrypt.auth_handler.time") - def test_poll_challenges_failure(self, unused_mock_time): + @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") + def test_poll_challenges_failure(self, unused_mock_time, unused_mock_zope): self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid self.assertRaises( errors.AuthorizationError, self.handler._poll_challenges, @@ -420,6 +421,54 @@ class IsPreferredTest(unittest.TestCase): self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P]))) +class ReportFailedChallsTest(unittest.TestCase): + """Tests for letsencrypt.auth_handler._report_failed_challs.""" + # pylint: disable=protected-access + + def setUp(self): + from letsencrypt import achallenges + + kwargs = { + "chall" : acme_util.SIMPLE_HTTP, + "uri": "uri", + "status": messages.STATUS_INVALID, + "error": messages.Error(typ="tls", detail="detail"), + } + + self.simple_http = achallenges.SimpleHTTP( + challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args + domain="example.com", + key=acme_util.KEY) + + kwargs["chall"] = acme_util.DVSNI + self.dvsni_same = achallenges.DVSNI( + challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args + domain="example.com", + key=acme_util.KEY) + + kwargs["error"] = messages.Error(typ="dnssec", detail="detail") + self.dvsni_diff = achallenges.DVSNI( + challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args + domain="foo.bar", + key=acme_util.KEY) + + @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") + def test_same_error_and_domain(self, mock_zope): + from letsencrypt import auth_handler + + auth_handler._report_failed_challs([self.simple_http, self.dvsni_same]) + call_list = mock_zope().add_message.call_args_list + self.assertTrue(len(call_list) == 1) + self.assertIn("Domains: example.com\n", call_list[0][0][0]) + + @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") + def test_different_errors_and_domains(self, mock_zope): + from letsencrypt import auth_handler + + auth_handler._report_failed_challs([self.simple_http, self.dvsni_diff]) + self.assertTrue(mock_zope().add_message.call_count == 2) + + def gen_auth_resp(chall_list): """Generate a dummy authorization response.""" return ["%s%s" % (chall.__class__.__name__, chall.domain) From d15a386f921bc4a75681c54c3a32c5b4cd20fad6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 24 Jun 2015 18:24:54 -0700 Subject: [PATCH 106/109] Incorporated jdkasten's feedback --- letsencrypt/auth_handler.py | 56 +++++++++++++------------- letsencrypt/reporter.py | 7 ++-- letsencrypt/tests/auth_handler_test.py | 2 +- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 019cb07dc..43f7b9fd2 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -487,32 +487,32 @@ def is_preferred(offered_challb, satisfied, _ERROR_HELP_COMMON = ( - 'To fix these errors, please make sure that your domain name was entered ' - 'correctly and the DNS A/AAAA record(s) for that domain contains the ' - 'right IP address.') + "To fix these errors, please make sure that your domain name was entered " + "correctly and the DNS A/AAAA record(s) for that domain contains the " + "right IP address.") _ERROR_HELP = { - 'connection' : - _ERROR_HELP_COMMON + ' Additionally, please check that your computer ' - 'has publicly routable IP address and no firewalls are preventing the ' - 'server from communicating with the client.', - 'dnssec' : - _ERROR_HELP_COMMON + ' Additionally, if you have DNSSEC enabled for ' - 'your domain, please ensure the signature is valid.', - 'malformed' : - 'To fix these errors, please make sure that you did not provide any ' - 'invalid information to the client and try running Let\'s Encrypt ' - 'again.', - 'serverInternal' : - 'Unfortunately, an error on the ACME server prevented you from completing ' - 'authorization. Please try again later.', - 'tls' : - _ERROR_HELP_COMMON + ' Additionally, please check that you have an up ' - 'to date TLS configuration that allows the server to communicate with ' - 'the Let\'s Encrypt client.', - 'unauthorized' : _ERROR_HELP_COMMON, - 'unknownHost' : _ERROR_HELP_COMMON,} + "connection" : + _ERROR_HELP_COMMON + " Additionally, please check that your computer " + "has publicly routable IP address and no firewalls are preventing the " + "server from communicating with the client.", + "dnssec" : + _ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for " + "your domain, please ensure the signature is valid.", + "malformed" : + "To fix these errors, please make sure that you did not provide any " + "invalid information to the client and try running Let's Encrypt " + "again.", + "serverInternal" : + "Unfortunately, an error on the ACME server prevented you from completing " + "authorization. Please try again later.", + "tls" : + _ERROR_HELP_COMMON + " Additionally, please check that you have an up " + "to date TLS configuration that allows the server to communicate with " + "the Let's Encrypt client.", + "unauthorized" : _ERROR_HELP_COMMON, + "unknownHost" : _ERROR_HELP_COMMON,} def _report_failed_challs(failed_achalls): @@ -552,12 +552,12 @@ def _generate_failed_chall_msg(failed_achalls): for achall in failed_achalls: problems.setdefault(achall.error.description, set()).add(achall.domain) for problem in problems: - msg.append('\n\nDomains: ') - msg.append(', '.join(sorted(problems[problem]))) - msg.append('\nError: {0}'.format(problem)) + msg.append("\n\nDomains: ") + msg.append(", ".join(sorted(problems[problem]))) + msg.append("\nError: {0}".format(problem)) if typ in _ERROR_HELP: - msg.append('\n\n') + msg.append("\n\n") msg.append(_ERROR_HELP[typ]) - return ''.join(msg) + return "".join(msg) diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 3045a7e19..dc3859535 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -46,10 +46,9 @@ class Reporter(object): printed if the program exits abnormally. """ - if msg: - assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY - self.messages.put(self._msg_type(priority, msg, on_crash)) - logging.info("Reporting to user: %s", msg) + assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY + self.messages.put(self._msg_type(priority, msg, on_crash)) + logging.info("Reporting to user: %s", msg) def atexit_print_messages(self, pid=os.getpid()): """Function to be registered with atexit to print messages. diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 15ce6a490..6a94baea7 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -459,7 +459,7 @@ class ReportFailedChallsTest(unittest.TestCase): auth_handler._report_failed_challs([self.simple_http, self.dvsni_same]) call_list = mock_zope().add_message.call_args_list self.assertTrue(len(call_list) == 1) - self.assertIn("Domains: example.com\n", call_list[0][0][0]) + self.assertTrue("Domains: example.com\n" in call_list[0][0][0]) @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") def test_different_errors_and_domains(self, mock_zope): From b9f2823d6b2ceb4f037bae527f7ae6f83389b0c2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 05:35:15 +0000 Subject: [PATCH 107/109] renewer: _paths_parser --- letsencrypt/renewer.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 7ac22138d..57df57ffc 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -96,12 +96,21 @@ def renew(cert, old_version): # (where fewer than all names were renewed) +def _paths_parser(parser): + add = parser.add_argument_group("paths").add_argument + add("--config-dir", default=cli.flag_default("config_dir"), + help=cli.config_help("config_dir")) + add("--work-dir", default=cli.flag_default("work_dir"), + help=cli.config_help("work_dir")) + return parser + + def _create_parser(): parser = argparse.ArgumentParser() #parser.add_argument("--cron", action="store_true", help="Run as cronjob.") # pylint: disable=protected-access - cli._paths_parser(parser.add_argument_group("paths")) - return parser + return _paths_parser(parser) + def main(config=None, args=sys.argv[1:]): """Main function for autorenewer script.""" From 4de60f68ab034eb5401d1b1dc5f0319dfa367e76 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 05:35:49 +0000 Subject: [PATCH 108/109] Pass cli_config to RenewableCert --- letsencrypt/renewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 57df57ffc..fb3490198 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -148,7 +148,7 @@ def main(config=None, args=sys.argv[1:]): # RenewableCert object for this cert at all, which could # dramatically improve performance for large deployments # where autorenewal is widely turned off. - cert = storage.RenewableCert(rc_config) + cert = storage.RenewableCert(rc_config, cli_config=cli_config) except ValueError: # This indicates an invalid renewal configuration file, such # as one missing a required parameter (in the future, perhaps From 38b497ef73dd8688dddfcbd67f81b67dfe0f6334 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 05:36:02 +0000 Subject: [PATCH 109/109] renewer: fix bug where renewer.conf wasn't read. --- letsencrypt/renewer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index fb3490198..4a1e96a65 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -137,8 +137,9 @@ def main(config=None, args=sys.argv[1:]): print "Processing", i if not i.endswith(".conf"): continue - rc_config = configobj.ConfigObj( - os.path.join(cli_config.renewal_configs_dir, i)) + rc_config = configobj.ConfigObj(cli_config.renewer_config_file) + rc_config.merge(configobj.ConfigObj( + os.path.join(cli_config.renewal_configs_dir, i))) try: # TODO: Before trying to initialize the RenewableCert object, # we could check here whether the combination of the config