From d48c560df13f283c83217d04bb09f81de042e48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6gl?= Date: Sun, 14 Feb 2016 22:21:25 +0100 Subject: [PATCH 01/34] correctly handle IPv6 and IPv4 addresses fix #1143 This commit correctly splits IPv6 addresses into the host and port parts. This will work for normal IPv4 and IPv6 addresses appended by a port number as well es for IPv6 addressess without a port, which should be the normal IPv6 usage. --- letsencrypt/plugins/common.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 37738f5c0..187d84daf 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -110,8 +110,17 @@ class Addr(object): @classmethod def fromstring(cls, str_addr): """Initialize Addr from string.""" - tup = str_addr.partition(':') - return cls((tup[0], tup[2])) + if str_addr.startswith('['): + # ipv6 addresses starts with [ + endIndex = str_addr.rfind(']') + host = str_addr[:endIndex + 1] + port = '' + if len(str_addr) > endIndex + 3 and str_addr[endIndex + 2] == ':': + port = str_addr[endIndex + 3:] + return cls((host, port)) + else: + tup = str_addr.partition(':') + return cls((tup[0], tup[2])) def __str__(self): if self.tup[1]: From b5bd330bd9322310b81856171b5fb0b4ec989be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6gl?= Date: Sun, 14 Feb 2016 23:08:58 +0100 Subject: [PATCH 02/34] add vhost test cases containing IPv6 addresses The two added vhost configuration files should test, whether a reversed order, i.e. first an IPv6 entry followed by an IPv4 one, or an IPv6 adress without a given port works correctly. --- .../{failing => passing}/ipv6-1143.conf | 0 .../{failing => passing}/ipv6-1143b.conf | 0 .../tests/apache-conf-files/passing/ipv6-1143c.conf | 9 +++++++++ .../tests/apache-conf-files/passing/ipv6-1143d.conf | 9 +++++++++ 4 files changed, 18 insertions(+) rename letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/{failing => passing}/ipv6-1143.conf (100%) rename letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/{failing => passing}/ipv6-1143b.conf (100%) create mode 100644 letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf create mode 100644 letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf similarity index 100% rename from letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf rename to letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf similarity index 100% rename from letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf rename to letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf new file mode 100644 index 000000000..f75dd7850 --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf @@ -0,0 +1,9 @@ + +DocumentRoot /xxxx/ +ServerName noodles.net.nz +ServerAlias www.noodles.net.nz +CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined + + AllowOverride All + + diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf new file mode 100644 index 000000000..f16b412da --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf @@ -0,0 +1,9 @@ + +DocumentRoot /xxxx/ +ServerName noodles.net.nz +ServerAlias www.noodles.net.nz +CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined + + AllowOverride All + + From d2a96efa8efb77e9d5423254d8e01bc6a634a112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6gl?= Date: Fri, 19 Feb 2016 23:39:22 +0100 Subject: [PATCH 03/34] add test cases --- letsencrypt/plugins/common_test.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 55319f0a0..5fdd57c5f 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -81,6 +81,9 @@ class AddrTest(unittest.TestCase): 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") + self.addr4 = Addr.fromstring("[fe00::1]") + self.addr5 = Addr.fromstring("[fe00::1]:*") + self.addr6 = Addr.fromstring("[fe00::1]:80") def test_fromstring(self): self.assertEqual(self.addr1.get_addr(), "192.168.1.1") @@ -89,22 +92,38 @@ class AddrTest(unittest.TestCase): self.assertEqual(self.addr2.get_port(), "*") self.assertEqual(self.addr3.get_addr(), "192.168.1.1") self.assertEqual(self.addr3.get_port(), "80") + self.assertEqual(self.addr4.get_addr(), "[fe00::1]") + self.assertEqual(self.addr4.get_port(), "") + self.assertEqual(self.addr5.get_addr(), "[fe00::1]") + self.assertEqual(self.addr5.get_port(), "*") + self.assertEqual(self.addr6.get_addr(), "[fe00::1]") + self.assertEqual(self.addr6.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") + self.assertEqual(str(self.addr4), "[fe00::1]") + self.assertEqual(str(self.addr5), "[fe00::1]:*") + self.assertEqual(str(self.addr6), "[fe00::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:*") + self.assertEqual(str(self.addr4.get_addr_obj("443")), "[fe00::1]:443") + self.assertEqual(str(self.addr5.get_addr_obj("")), "[fe00::1]") + self.assertEqual(str(self.addr4.get_addr_obj("*")), "[fe00::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) + self.assertEqual(self.addr4, self.addr4.get_addr_obj("")) + self.assertNotEqual(self.addr4, self.addr5) + self.assertFalse(self.addr4 == 3333) + def test_set_inclusion(self): from letsencrypt.plugins.common import Addr set_a = set([self.addr1, self.addr2]) @@ -114,6 +133,13 @@ class AddrTest(unittest.TestCase): self.assertEqual(set_a, set_b) + set_c = set([self.addr4, self.addr5]) + addr4b = Addr.fromstring("[fe00::1]") + addr5b = Addr.fromstring("[fe00::1]:*") + set_d = set([addr4b, addr5b]) + + self.assertEqual(set_c, set_d) + class TLSSNI01Test(unittest.TestCase): """Tests for letsencrypt.plugins.common.TLSSNI01.""" From 9b08fd3964e17ca94353f4757946533d41d5edd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6gl?= Date: Fri, 19 Feb 2016 23:40:44 +0100 Subject: [PATCH 04/34] correctly parse ipv6 address This commit fixes the wrong used indexes for parsing the ipv6 address. --- letsencrypt/plugins/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 187d84daf..5a8effc2b 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -115,8 +115,8 @@ class Addr(object): endIndex = str_addr.rfind(']') host = str_addr[:endIndex + 1] port = '' - if len(str_addr) > endIndex + 3 and str_addr[endIndex + 2] == ':': - port = str_addr[endIndex + 3:] + if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':': + port = str_addr[endIndex + 2:] return cls((host, port)) else: tup = str_addr.partition(':') From 50881cbb35d8e4a814691eb448ab3363878c01ae Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 11 Mar 2016 14:49:12 -0800 Subject: [PATCH 05/34] Start splitting plugins.selection out of cli --- letsencrypt/cli.py | 152 ++----------------------------- letsencrypt/main.py | 9 +- letsencrypt/plugins/selection.py | 146 +++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 150 deletions(-) create mode 100644 letsencrypt/plugins/selection.py diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index bea8b198d..2b45b180d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -13,7 +13,6 @@ import configargparse import OpenSSL import six -import letsencrypt from letsencrypt import constants from letsencrypt import crypto_util @@ -23,6 +22,7 @@ from letsencrypt import le_util from letsencrypt.display import ops as display_ops from letsencrypt.plugins import disco as plugins_disco +from letsencrypt.plugin.selection import cli_plugin_requests logger = logging.getLogger(__name__) @@ -33,9 +33,10 @@ helpful_parser = None # For help strings, figure out how the user ran us. # When invoked from letsencrypt-auto, sys.argv[0] is something like: # "/home/user/.local/share/letsencrypt/bin/letsencrypt" -# Note that this won't work if the user set VENV_PATH or XDG_DATA_HOME before running -# letsencrypt-auto (and sudo stops us from seeing if they did), so it should only be used -# for purposes where inability to detect letsencrypt-auto fails safely +# Note that this won't work if the user set VENV_PATH or XDG_DATA_HOME before +# running letsencrypt-auto (and sudo stops us from seeing if they did), so it +# should only be used for purposes where inability to detect letsencrypt-auto +# fails safely fragment = os.path.join(".local", "share", "letsencrypt") cli_command = "letsencrypt-auto" if fragment in sys.argv[0] else "letsencrypt" @@ -99,145 +100,6 @@ def usage_strings(plugins): return USAGE % (apache_doc, nginx_doc), SHORT_USAGE -def diagnose_configurator_problem(cfg_type, requested, plugins): - """ - Raise the most helpful error message about a plugin being unavailable - - :param str cfg_type: either "installer" or "authenticator" - :param str requested: the plugin that was requested - :param .PluginsRegistry plugins: available plugins - - :raises error.PluginSelectionError: if there was a problem - """ - - if requested: - if requested not in plugins: - msg = "The requested {0} plugin does not appear to be installed".format(requested) - else: - msg = ("The {0} plugin is not working; there may be problems with " - "your existing configuration.\nThe error was: {1!r}" - .format(requested, plugins[requested].problem)) - elif cfg_type == "installer": - if os.path.exists("/etc/debian_version"): - # Debian... installers are at least possible - msg = ('No installers seem to be present and working on your system; ' - 'fix that or try running letsencrypt with the "certonly" command') - else: - # XXX update this logic as we make progress on #788 and nginx support - msg = ('No installers are available on your OS yet; try running ' - '"letsencrypt-auto certonly" to get a cert you can install manually') - else: - msg = "{0} could not be determined or is not installed".format(cfg_type) - raise errors.PluginSelectionError(msg) - - -def set_configurator(previously, now): - """ - Setting configurators multiple ways is okay, as long as they all agree - :param str previously: previously identified request for the installer/authenticator - :param str requested: the request currently being processed - """ - if now is None: - # we're not actually setting anything - return previously - if previously: - if previously != now: - msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}" - raise errors.PluginSelectionError(msg.format(repr(previously), repr(now))) - return now - - -def cli_plugin_requests(config): - """ - Figure out which plugins the user requested with CLI and config options - - :returns: (requested authenticator string or None, requested installer string or None) - :rtype: tuple - """ - req_inst = req_auth = config.configurator - req_inst = set_configurator(req_inst, config.installer) - req_auth = set_configurator(req_auth, config.authenticator) - if config.nginx: - req_inst = set_configurator(req_inst, "nginx") - req_auth = set_configurator(req_auth, "nginx") - if config.apache: - req_inst = set_configurator(req_inst, "apache") - req_auth = set_configurator(req_auth, "apache") - if config.standalone: - req_auth = set_configurator(req_auth, "standalone") - if config.webroot: - req_auth = set_configurator(req_auth, "webroot") - if config.manual: - req_auth = set_configurator(req_auth, "manual") - logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) - return req_auth, req_inst - - -noninstaller_plugins = ["webroot", "manual", "standalone"] - - -def choose_configurator_plugins(config, plugins, verb): - """ - Figure out which configurator we're going to use, modifies - config.authenticator and config.istaller strings to reflect that choice if - necessary. - - :raises errors.PluginSelectionError if there was a problem - - :returns: (an `IAuthenticator` or None, an `IInstaller` or None) - :rtype: tuple - """ - - req_auth, req_inst = cli_plugin_requests(config) - - # Which plugins do we need? - if verb == "run": - need_inst = need_auth = True - if req_auth in noninstaller_plugins and not req_inst: - msg = ('With the {0} plugin, you probably want to use the "certonly" command, eg:{1}' - '{1} {2} certonly --{0}{1}{1}' - '(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins' - '{1} and "--help plugins" for more information.)'.format( - req_auth, os.linesep, cli_command)) - - raise errors.MissingCommandlineFlag(msg) - else: - need_inst = need_auth = False - if verb == "certonly": - need_auth = True - if verb == "install": - need_inst = True - if config.authenticator: - logger.warn("Specifying an authenticator doesn't make sense in install mode") - - # Try to meet the user's request and/or ask them to pick plugins - authenticator = installer = None - if verb == "run" and req_auth == req_inst: - # Unless the user has explicitly asked for different auth/install, - # only consider offering a single choice - authenticator = installer = display_ops.pick_configurator(config, req_inst, plugins) - else: - if need_inst or req_inst: - installer = display_ops.pick_installer(config, req_inst, plugins) - if need_auth: - authenticator = display_ops.pick_authenticator(config, req_auth, plugins) - logger.debug("Selected authenticator %s and installer %s", authenticator, installer) - - # Report on any failures - if need_inst and not installer: - diagnose_configurator_problem("installer", req_inst, plugins) - if need_auth and not authenticator: - diagnose_configurator_problem("authenticator", req_auth, plugins) - - record_chosen_plugins(config, plugins, authenticator, installer) - return installer, authenticator - - -def record_chosen_plugins(config, plugins, auth, inst): - "Update the config entries to reflect the plugins we actually selected." - cn = config.namespace - cn.authenticator = plugins.find_init(auth).name if auth else "none" - cn.installer = plugins.find_init(inst).name if inst else "none" def set_by_cli(var): @@ -256,7 +118,7 @@ def set_by_cli(var): detector = set_by_cli.detector = prepare_and_parse_args( plugins, reconstructed_args, detect_defaults=True) # propagate plugin requests: eg --standalone modifies config.authenticator - auth, inst = cli_plugin_requests(detector) + auth, inst = plugin_selection.cli_plugin_requests(detector) detector.authenticator = auth if auth else "" detector.installer = inst if inst else "" logger.debug("Default Detector is %r", detector) @@ -308,7 +170,7 @@ def flag_default(name): def config_help(name, hidden=False): - """Help message for `.IConfig` attribute.""" + """Extract the help message for an `.IConfig` attribute.""" if hidden: return argparse.SUPPRESS else: diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 8d59993df..48e6e8505 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -6,8 +6,6 @@ import os import sys import zope.component -import letsencrypt - from letsencrypt import account from letsencrypt import client from letsencrypt import cli @@ -25,6 +23,7 @@ from letsencrypt import storage from letsencrypt.display import util as display_util, ops as display_ops from letsencrypt.plugins import disco as plugins_disco +from letsencrypt.plugins.selection import choose_configurator_plugins import traceback import logging.handlers @@ -404,7 +403,7 @@ def install(config, plugins): # this function ... try: - installer, _ = cli.choose_configurator_plugins(config, plugins, "install") + installer, _ = choose_configurator_plugins(config, plugins, "install") except errors.PluginSelectionError as e: return e.message @@ -479,7 +478,7 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals # TODO: Make run as close to auth + install as possible # Possible difficulties: config.csr was hacked into auth try: - installer, authenticator = cli.choose_configurator_plugins(config, plugins, "run") + installer, authenticator = choose_configurator_plugins(config, plugins, "run") except errors.PluginSelectionError as e: return e.message @@ -509,7 +508,7 @@ def obtain_cert(config, plugins, lineage=None): # pylint: disable=too-many-locals try: # installers are used in auth mode to determine domain names - installer, authenticator = cli.choose_configurator_plugins(config, plugins, "certonly") + installer, authenticator = choose_configurator_plugins(config, plugins, "certonly") except errors.PluginSelectionError as e: logger.info("Could not choose appropriate plugin: %s", e) raise diff --git a/letsencrypt/plugins/selection.py b/letsencrypt/plugins/selection.py new file mode 100644 index 000000000..fc7274a24 --- /dev/null +++ b/letsencrypt/plugins/selection.py @@ -0,0 +1,146 @@ +from __future__ import print_function +import os +from letsencrypt import errors +from letsencrypt.display import ops as display_ops + +logger = logging.getLogger(__name__) + +noninstaller_plugins = ["webroot", "manual", "standalone"] + +def record_chosen_plugins(config, plugins, auth, inst): + "Update the config entries to reflect the plugins we actually selected." + cn = config.namespace + cn.authenticator = plugins.find_init(auth).name if auth else "none" + cn.installer = plugins.find_init(inst).name if inst else "none" + + +def choose_configurator_plugins(config, plugins, verb): + """ + Figure out which configurator we're going to use, modifies + config.authenticator and config.istaller strings to reflect that choice if + necessary. + + :raises errors.PluginSelectionError if there was a problem + + :returns: (an `IAuthenticator` or None, an `IInstaller` or None) + :rtype: tuple + """ + + req_auth, req_inst = cli_plugin_requests(config) + + # Which plugins do we need? + if verb == "run": + need_inst = need_auth = True + from letsencrypt.cli import cli_command + if req_auth in noninstaller_plugins and not req_inst: + msg = ('With the {0} plugin, you probably want to use the "certonly" command, eg:{1}' + '{1} {2} certonly --{0}{1}{1}' + '(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins' + '{1} and "--help plugins" for more information.)'.format( + req_auth, os.linesep, cli_command)) + + raise errors.MissingCommandlineFlag(msg) + else: + need_inst = need_auth = False + if verb == "certonly": + need_auth = True + if verb == "install": + need_inst = True + if config.authenticator: + logger.warn("Specifying an authenticator doesn't make sense in install mode") + + # Try to meet the user's request and/or ask them to pick plugins + authenticator = installer = None + if verb == "run" and req_auth == req_inst: + # Unless the user has explicitly asked for different auth/install, + # only consider offering a single choice + authenticator = installer = display_ops.pick_configurator(config, req_inst, plugins) + else: + if need_inst or req_inst: + installer = display_ops.pick_installer(config, req_inst, plugins) + if need_auth: + authenticator = display_ops.pick_authenticator(config, req_auth, plugins) + logger.debug("Selected authenticator %s and installer %s", authenticator, installer) + + # Report on any failures + if need_inst and not installer: + diagnose_configurator_problem("installer", req_inst, plugins) + if need_auth and not authenticator: + diagnose_configurator_problem("authenticator", req_auth, plugins) + + record_chosen_plugins(config, plugins, authenticator, installer) + return installer, authenticator + + +def set_configurator(previously, now): + """ + Setting configurators multiple ways is okay, as long as they all agree + :param str previously: previously identified request for the installer/authenticator + :param str requested: the request currently being processed + """ + if now is None: + # we're not actually setting anything + return previously + if previously: + if previously != now: + msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}" + raise errors.PluginSelectionError(msg.format(repr(previously), repr(now))) + return now + + +def cli_plugin_requests(config): + """ + Figure out which plugins the user requested with CLI and config options + + :returns: (requested authenticator string or None, requested installer string or None) + :rtype: tuple + """ + req_inst = req_auth = config.configurator + req_inst = set_configurator(req_inst, config.installer) + req_auth = set_configurator(req_auth, config.authenticator) + if config.nginx: + req_inst = set_configurator(req_inst, "nginx") + req_auth = set_configurator(req_auth, "nginx") + if config.apache: + req_inst = set_configurator(req_inst, "apache") + req_auth = set_configurator(req_auth, "apache") + if config.standalone: + req_auth = set_configurator(req_auth, "standalone") + if config.webroot: + req_auth = set_configurator(req_auth, "webroot") + if config.manual: + req_auth = set_configurator(req_auth, "manual") + logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) + return req_auth, req_inst + + +def diagnose_configurator_problem(cfg_type, requested, plugins): + """ + Raise the most helpful error message about a plugin being unavailable + + :param str cfg_type: either "installer" or "authenticator" + :param str requested: the plugin that was requested + :param .PluginsRegistry plugins: available plugins + + :raises error.PluginSelectionError: if there was a problem + """ + + if requested: + if requested not in plugins: + msg = "The requested {0} plugin does not appear to be installed".format(requested) + else: + msg = ("The {0} plugin is not working; there may be problems with " + "your existing configuration.\nThe error was: {1!r}" + .format(requested, plugins[requested].problem)) + elif cfg_type == "installer": + if os.path.exists("/etc/debian_version"): + # Debian... installers are at least possible + msg = ('No installers seem to be present and working on your system; ' + 'fix that or try running letsencrypt with the "certonly" command') + else: + # XXX update this logic as we make progress on #788 and nginx support + msg = ('No installers are available on your OS yet; try running ' + '"letsencrypt-auto certonly" to get a cert you can install manually') + else: + msg = "{0} could not be determined or is not installed".format(cfg_type) + raise errors.PluginSelectionError(msg) From c6fece8b404e5662772484d394490bf7ff3444b4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 11 Mar 2016 15:24:57 -0800 Subject: [PATCH 06/34] Also move plugin selection logic from display.ops --- letsencrypt/client.py | 5 +- letsencrypt/display/ops.py | 136 ++------------------------ letsencrypt/plugins/selection.py | 131 ++++++++++++++++++++++++- letsencrypt/tests/display/ops_test.py | 12 +-- 4 files changed, 143 insertions(+), 141 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 6134c4e6e..1655c1fa5 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -11,8 +11,6 @@ from acme import client as acme_client from acme import jose from acme import messages -import letsencrypt - from letsencrypt import account from letsencrypt import auth_handler from letsencrypt import configuration @@ -27,6 +25,7 @@ from letsencrypt import storage from letsencrypt.display import ops as display_ops from letsencrypt.display import enhancements +from letsencrypt.plugins import selection as plugin_selection logger = logging.getLogger(__name__) @@ -524,7 +523,7 @@ def rollback(default_installer, checkpoints, config, plugins): """ # Misconfigurations are only a slight problems... allow the user to rollback - installer = display_ops.pick_installer( + installer = plugin_selection.pick_installer( config, default_installer, plugins, question="Which installer " "should be used for rollback?") diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index f0dec8b06..d60073ce0 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -8,131 +8,13 @@ from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt.display import util as display_util +import letsencrypt.plugins.selection logger = logging.getLogger(__name__) # Define a helper function to avoid verbose code -util = zope.component.getUtility - - -def choose_plugin(prepared, question): - """Allow the user to choose their plugin. - - :param list prepared: List of `~.PluginEntryPoint`. - :param str question: Question to be presented to the user. - - :returns: Plugin entry point chosen by the user. - :rtype: `~.PluginEntryPoint` - - """ - opts = [plugin_ep.description_with_name + - (" [Misconfigured]" if plugin_ep.misconfigured else "") - for plugin_ep in prepared] - - while True: - disp = util(interfaces.IDisplay) - code, index = disp.menu(question, opts, help_label="More Info") - - if code == display_util.OK: - plugin_ep = prepared[index] - if plugin_ep.misconfigured: - util(interfaces.IDisplay).notification( - "The selected plugin encountered an error while parsing " - "your server configuration and cannot be used. The error " - "was:\n\n{0}".format(plugin_ep.prepare()), - height=display_util.HEIGHT, pause=False) - else: - return plugin_ep - elif code == display_util.HELP: - if prepared[index].misconfigured: - msg = "Reported Error: %s" % prepared[index].prepare() - else: - msg = prepared[index].init().more_info() - util(interfaces.IDisplay).notification( - msg, height=display_util.HEIGHT) - else: - return None - - -def pick_plugin(config, default, plugins, question, ifaces): - """Pick plugin. - - :param letsencrypt.interfaces.IConfig: Configuration - :param str default: Plugin name supplied by user or ``None``. - :param letsencrypt.plugins.disco.PluginsRegistry plugins: - All plugins registered as entry points. - :param str question: Question to be presented to the user in case - multiple candidates are found. - :param list ifaces: Interfaces that plugins must provide. - - :returns: Initialized plugin. - :rtype: IPlugin - - """ - if default is not None: - # throw more UX-friendly error if default not in plugins - filtered = plugins.filter(lambda p_ep: p_ep.name == default) - else: - if config.noninteractive_mode: - # it's really bad to auto-select the single available plugin in - # non-interactive mode, because an update could later add a second - # available plugin - raise errors.MissingCommandlineFlag( - "Missing command line flags. For non-interactive execution, " - "you will need to specify a plugin on the command line. Run " - "with '--help plugins' to see a list of options, and see " - "https://eff.org/letsencrypt-plugins for more detail on what " - "the plugins do and how to use them.") - - filtered = plugins.visible().ifaces(ifaces) - - filtered.init(config) - verified = filtered.verify(ifaces) - verified.prepare() - prepared = verified.available() - - if len(prepared) > 1: - logger.debug("Multiple candidate plugins: %s", prepared) - plugin_ep = choose_plugin(prepared.values(), question) - if plugin_ep is None: - return None - else: - return plugin_ep.init() - elif len(prepared) == 1: - plugin_ep = prepared.values()[0] - logger.debug("Single candidate plugin: %s", plugin_ep) - if plugin_ep.misconfigured: - return None - return plugin_ep.init() - else: - logger.debug("No candidate plugin") - return None - - -def pick_authenticator( - config, default, plugins, question="How would you " - "like to authenticate with the Let's Encrypt CA?"): - """Pick authentication plugin.""" - return pick_plugin( - config, default, plugins, question, (interfaces.IAuthenticator,)) - - -def pick_installer(config, default, plugins, - question="How would you like to install certificates?"): - """Pick installer plugin.""" - return pick_plugin( - config, default, plugins, question, (interfaces.IInstaller,)) - - -def pick_configurator( - config, default, plugins, - question="How would you like to authenticate and install " - "certificates?"): - """Pick configurator plugin.""" - return pick_plugin( - config, default, plugins, question, - (interfaces.IAuthenticator, interfaces.IInstaller)) +z_util = zope.component.getUtility def get_email(more=False, invalid=False): @@ -182,7 +64,7 @@ def choose_account(accounts): # Note this will get more complicated once we start recording authorizations labels = [acc.slug for acc in accounts] - code, index = util(interfaces.IDisplay).menu( + code, index = z_util(interfaces.IDisplay).menu( "Please choose an account", labels) if code == display_util.OK: return accounts[index] @@ -208,7 +90,7 @@ def choose_names(installer): names = get_valid_domains(domains) if not names: - manual = util(interfaces.IDisplay).yesno( + manual = z_util(interfaces.IDisplay).yesno( "No names were found in your configuration files.{0}You should " "specify ServerNames in your config files in order to allow for " "accurate installation of your certificate.{0}" @@ -256,7 +138,7 @@ def _filter_names(names): :rtype: tuple """ - code, names = util(interfaces.IDisplay).checklist( + code, names = z_util(interfaces.IDisplay).checklist( "Which names would you like to activate HTTPS for?", tags=names, cli_flag="--domains") return code, [str(s) for s in names] @@ -265,7 +147,7 @@ def _filter_names(names): def _choose_names_manually(): """Manually input names for those without an installer.""" - code, input_ = util(interfaces.IDisplay).input( + code, input_ = z_util(interfaces.IDisplay).input( "Please enter in your domain name(s) (comma and/or space separated) ", cli_flag="--domains") @@ -300,7 +182,7 @@ def _choose_names_manually(): if retry_message: # We had error in input - retry = util(interfaces.IDisplay).yesno(retry_message) + retry = z_util(interfaces.IDisplay).yesno(retry_message) if retry: return _choose_names_manually() else: @@ -316,7 +198,7 @@ def success_installation(domains): :param list domains: domain names which were enabled """ - util(interfaces.IDisplay).notification( + z_util(interfaces.IDisplay).notification( "Congratulations! You have successfully enabled {0}{1}{1}" "You should test your configuration at:{1}{2}".format( _gen_https_names(domains), @@ -335,7 +217,7 @@ def success_renewal(domains, action): :param str action: can be "reinstall" or "renew" """ - util(interfaces.IDisplay).notification( + z_util(interfaces.IDisplay).notification( "Your existing certificate has been successfully {3}ed, and the " "new certificate has been installed.{1}{1}" "The new certificate covers the following domains: {0}{1}{1}" diff --git a/letsencrypt/plugins/selection.py b/letsencrypt/plugins/selection.py index fc7274a24..bf6187950 100644 --- a/letsencrypt/plugins/selection.py +++ b/letsencrypt/plugins/selection.py @@ -1,7 +1,128 @@ from __future__ import print_function import os -from letsencrypt import errors -from letsencrypt.display import ops as display_ops +from letsencrypt import errors, interfaces +from letsencrypt.display import util as display_util + +logger = logging.getLogger(__name__) +z_util = zope.component.getUtility + +def pick_configurator( + config, default, plugins, + question="How would you like to authenticate and install " + "certificates?"): + """Pick configurator plugin.""" + return pick_plugin( + config, default, plugins, question, + (interfaces.IAuthenticator, interfaces.IInstaller)) + + +def pick_installer(config, default, plugins, + question="How would you like to install certificates?"): + """Pick installer plugin.""" + return pick_plugin( + config, default, plugins, question, (interfaces.IInstaller,)) + + +def pick_authenticator( + config, default, plugins, question="How would you " + "like to authenticate with the Let's Encrypt CA?"): + """Pick authentication plugin.""" + return pick_plugin( + config, default, plugins, question, (interfaces.IAuthenticator,)) + + +def pick_plugin(config, default, plugins, question, ifaces): + """Pick plugin. + + :param letsencrypt.interfaces.IConfig: Configuration + :param str default: Plugin name supplied by user or ``None``. + :param letsencrypt.plugins.disco.PluginsRegistry plugins: + All plugins registered as entry points. + :param str question: Question to be presented to the user in case + multiple candidates are found. + :param list ifaces: Interfaces that plugins must provide. + + :returns: Initialized plugin. + :rtype: IPlugin + + """ + if default is not None: + # throw more UX-friendly error if default not in plugins + filtered = plugins.filter(lambda p_ep: p_ep.name == default) + else: + if config.noninteractive_mode: + # it's really bad to auto-select the single available plugin in + # non-interactive mode, because an update could later add a second + # available plugin + raise errors.MissingCommandlineFlag( + "Missing command line flags. For non-interactive execution, " + "you will need to specify a plugin on the command line. Run " + "with '--help plugins' to see a list of options, and see " + "https://eff.org/letsencrypt-plugins for more detail on what " + "the plugins do and how to use them.") + + filtered = plugins.visible().ifaces(ifaces) + + filtered.init(config) + verified = filtered.verify(ifaces) + verified.prepare() + prepared = verified.available() + + if len(prepared) > 1: + logger.debug("Multiple candidate plugins: %s", prepared) + plugin_ep = choose_plugin(prepared.values(), question) + if plugin_ep is None: + return None + else: + return plugin_ep.init() + elif len(prepared) == 1: + plugin_ep = prepared.values()[0] + logger.debug("Single candidate plugin: %s", plugin_ep) + if plugin_ep.misconfigured: + return None + return plugin_ep.init() + else: + logger.debug("No candidate plugin") + return None + + +def choose_plugin(prepared, question): + """Allow the user to choose their plugin. + + :param list prepared: List of `~.PluginEntryPoint`. + :param str question: Question to be presented to the user. + + :returns: Plugin entry point chosen by the user. + :rtype: `~.PluginEntryPoint` + + """ + opts = [plugin_ep.description_with_name + + (" [Misconfigured]" if plugin_ep.misconfigured else "") + for plugin_ep in prepared] + + while True: + disp = z_util(interfaces.IDisplay) + code, index = disp.menu(question, opts, help_label="More Info") + + if code == display_util.OK: + plugin_ep = prepared[index] + if plugin_ep.misconfigured: + z_util(interfaces.IDisplay).notification( + "The selected plugin encountered an error while parsing " + "your server configuration and cannot be used. The error " + "was:\n\n{0}".format(plugin_ep.prepare()), + height=display_util.HEIGHT, pause=False) + else: + return plugin_ep + elif code == display_util.HELP: + if prepared[index].misconfigured: + msg = "Reported Error: %s" % prepared[index].prepare() + else: + msg = prepared[index].init().more_info() + z_util(interfaces.IDisplay).notification( + msg, height=display_util.HEIGHT) + else: + return None logger = logging.getLogger(__name__) @@ -54,12 +175,12 @@ def choose_configurator_plugins(config, plugins, verb): if verb == "run" and req_auth == req_inst: # Unless the user has explicitly asked for different auth/install, # only consider offering a single choice - authenticator = installer = display_ops.pick_configurator(config, req_inst, plugins) + authenticator = installer = pick_configurator(config, req_inst, plugins) else: if need_inst or req_inst: - installer = display_ops.pick_installer(config, req_inst, plugins) + installer = pick_installer(config, req_inst, plugins) if need_auth: - authenticator = display_ops.pick_authenticator(config, req_auth, plugins) + authenticator = pick_authenticator(config, req_auth, plugins) logger.debug("Selected authenticator %s and installer %s", authenticator, installer) # Report on any failures diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 9a39e1538..a7e8e1d74 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -76,7 +76,7 @@ class PickPluginTest(unittest.TestCase): self.ifaces = [] def _call(self): - from letsencrypt.display.ops import pick_plugin + from letsencrypt.plugins.selection import pick_plugin return pick_plugin(self.config, self.default, self.reg, self.question, self.ifaces) @@ -149,17 +149,17 @@ class ConveniencePickPluginTest(unittest.TestCase): config, default, plugins, "Question?", ifaces) def test_authenticator(self): - from letsencrypt.display.ops import pick_authenticator + from letsencrypt.plugins.selection import pick_authenticator self._test(pick_authenticator, (interfaces.IAuthenticator,)) def test_installer(self): - from letsencrypt.display.ops import pick_installer + from letsencrypt.plugins.selection import pick_installer self._test(pick_installer, (interfaces.IInstaller,)) def test_configurator(self): - from letsencrypt.display.ops import pick_configurator - self._test(pick_configurator, ( - interfaces.IAuthenticator, interfaces.IInstaller)) + from letsencrypt.plugins.selection import pick_configurator + self._test(pick_configurator, + (interfaces.IAuthenticator, interfaces.IInstaller)) class GetEmailTest(unittest.TestCase): From 1c652716a25e0369fe5c3088b50a7c6032eb42a4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 11 Mar 2016 15:37:24 -0800 Subject: [PATCH 07/34] Start splitting out tests for plugins.selection --- letsencrypt/cli.py | 6 +- letsencrypt/client.py | 2 + letsencrypt/display/ops.py | 2 - letsencrypt/main.py | 2 + letsencrypt/plugins/selection.py | 10 +- letsencrypt/plugins/selection_test.py | 149 ++++++++++++++++++++++++++ letsencrypt/tests/display/ops_test.py | 140 ------------------------ 7 files changed, 165 insertions(+), 146 deletions(-) create mode 100644 letsencrypt/plugins/selection_test.py diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 4788a41e5..cb6f66be9 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -13,6 +13,7 @@ import configargparse import OpenSSL import six +import letsencrypt from letsencrypt import constants from letsencrypt import crypto_util @@ -20,9 +21,8 @@ from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util -from letsencrypt.display import ops as display_ops from letsencrypt.plugins import disco as plugins_disco -from letsencrypt.plugin.selection import cli_plugin_requests +from letsencrypt.plugins.selection import cli_plugin_requests logger = logging.getLogger(__name__) @@ -118,7 +118,7 @@ def set_by_cli(var): detector = set_by_cli.detector = prepare_and_parse_args( plugins, reconstructed_args, detect_defaults=True) # propagate plugin requests: eg --standalone modifies config.authenticator - auth, inst = plugin_selection.cli_plugin_requests(detector) + auth, inst = cli_plugin_requests(detector) detector.authenticator = auth if auth else "" detector.installer = inst if inst else "" logger.debug("Default Detector is %r", detector) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 1655c1fa5..d559b6311 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -11,6 +11,8 @@ from acme import client as acme_client from acme import jose from acme import messages +import letsencrypt + from letsencrypt import account from letsencrypt import auth_handler from letsencrypt import configuration diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index d60073ce0..302051b1b 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -8,8 +8,6 @@ from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt.display import util as display_util -import letsencrypt.plugins.selection - logger = logging.getLogger(__name__) diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 48e6e8505..153e118a4 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -6,6 +6,8 @@ import os import sys import zope.component +import letsencrypt + from letsencrypt import account from letsencrypt import client from letsencrypt import cli diff --git a/letsencrypt/plugins/selection.py b/letsencrypt/plugins/selection.py index bf6187950..9c5a7804a 100644 --- a/letsencrypt/plugins/selection.py +++ b/letsencrypt/plugins/selection.py @@ -1,6 +1,14 @@ +"""Decide which plugins to use for authentication & installation""" from __future__ import print_function + import os -from letsencrypt import errors, interfaces +import logging + +import zope.component + +from letsencrypt import errors +from letsencrypt import interfaces + from letsencrypt.display import util as display_util logger = logging.getLogger(__name__) diff --git a/letsencrypt/plugins/selection_test.py b/letsencrypt/plugins/selection_test.py new file mode 100644 index 000000000..0beaab076 --- /dev/null +++ b/letsencrypt/plugins/selection_test.py @@ -0,0 +1,149 @@ +"""Tests for letsenecrypt.plugins.selection""" +import sys +import unittest + +import mock +import zope.component + +from letsencrypt.display import util as display_util +from letsencrypt import interfaces + + +class ConveniencePickPluginTest(unittest.TestCase): + """Tests for letsencrypt.plugins.selection.pick_*.""" + + def _test(self, fun, ifaces): + config = mock.Mock() + default = mock.Mock() + plugins = mock.Mock() + + with mock.patch("letsencrypt.plugins.selection.pick_plugin") as mock_p: + mock_p.return_value = "foo" + self.assertEqual("foo", fun(config, default, plugins, "Question?")) + mock_p.assert_called_once_with( + config, default, plugins, "Question?", ifaces) + + def test_authenticator(self): + from letsencrypt.plugins.selection import pick_authenticator + self._test(pick_authenticator, (interfaces.IAuthenticator,)) + + def test_installer(self): + from letsencrypt.plugins.selection import pick_installer + self._test(pick_installer, (interfaces.IInstaller,)) + + def test_configurator(self): + from letsencrypt.plugins.selection import pick_configurator + self._test(pick_configurator, + (interfaces.IAuthenticator, interfaces.IInstaller)) + + +class PickPluginTest(unittest.TestCase): + """Tests for letsencrypt.plugins.selection.pick_plugin.""" + + def setUp(self): + self.config = mock.Mock(noninteractive_mode=False) + self.default = None + self.reg = mock.MagicMock() + self.question = "Question?" + self.ifaces = [] + + def _call(self): + from letsencrypt.plugins.selection import pick_plugin + return pick_plugin(self.config, self.default, self.reg, + self.question, self.ifaces) + + def test_default_provided(self): + self.default = "foo" + self._call() + self.assertEqual(1, self.reg.filter.call_count) + + def test_no_default(self): + self._call() + self.assertEqual(1, self.reg.visible().ifaces.call_count) + + def test_no_candidate(self): + self.assertTrue(self._call() is None) + + def test_single(self): + plugin_ep = mock.MagicMock() + plugin_ep.init.return_value = "foo" + plugin_ep.misconfigured = False + + self.reg.visible().ifaces().verify().available.return_value = { + "bar": plugin_ep} + self.assertEqual("foo", self._call()) + + def test_single_misconfigured(self): + plugin_ep = mock.MagicMock() + plugin_ep.init.return_value = "foo" + plugin_ep.misconfigured = True + + self.reg.visible().ifaces().verify().available.return_value = { + "bar": plugin_ep} + self.assertTrue(self._call() is None) + + def test_multiple(self): + plugin_ep = mock.MagicMock() + plugin_ep.init.return_value = "foo" + self.reg.visible().ifaces().verify().available.return_value = { + "bar": plugin_ep, + "baz": plugin_ep, + } + with mock.patch("letsencrypt.plugins.selection.choose_plugin") as mock_choose: + mock_choose.return_value = plugin_ep + self.assertEqual("foo", self._call()) + mock_choose.assert_called_once_with( + [plugin_ep, plugin_ep], self.question) + + def test_choose_plugin_none(self): + self.reg.visible().ifaces().verify().available.return_value = { + "bar": None, + "baz": None, + } + + with mock.patch("letsencrypt.plugins.selection.choose_plugin") as mock_choose: + mock_choose.return_value = None + self.assertTrue(self._call() is None) + + +class ChoosePluginTest(unittest.TestCase): + """Tests for letsencrypt.plugins.selection.choose_plugin.""" + + def setUp(self): + zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + self.mock_apache = mock.Mock( + description_with_name="a", misconfigured=True) + self.mock_stand = mock.Mock( + description_with_name="s", misconfigured=False) + self.mock_stand.init().more_info.return_value = "standalone" + self.plugins = [ + self.mock_apache, + self.mock_stand, + ] + + def _call(self): + from letsencrypt.plugins.selection import choose_plugin + return choose_plugin(self.plugins, "Question?") + + @mock.patch("letsencrypt.plugins.selection.z_util") + def test_selection(self, mock_util): + mock_util().menu.side_effect = [(display_util.OK, 0), + (display_util.OK, 1)] + self.assertEqual(self.mock_stand, self._call()) + self.assertEqual(mock_util().notification.call_count, 1) + + @mock.patch("letsencrypt.plugins.selection.z_util") + def test_more_info(self, mock_util): + mock_util().menu.side_effect = [ + (display_util.HELP, 0), + (display_util.HELP, 1), + (display_util.OK, 1), + ] + + self.assertEqual(self.mock_stand, self._call()) + self.assertEqual(mock_util().notification.call_count, 2) + + @mock.patch("letsencrypt.plugins.selection.z_util") + def test_no_choice(self, mock_util): + mock_util().menu.return_value = (display_util.CANCEL, 0) + self.assertTrue(self._call() is None) diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index a7e8e1d74..8a52c872b 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -22,146 +22,6 @@ from letsencrypt.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) -class ChoosePluginTest(unittest.TestCase): - """Tests for letsencrypt.display.ops.choose_plugin.""" - - def setUp(self): - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) - self.mock_apache = mock.Mock( - description_with_name="a", misconfigured=True) - self.mock_stand = mock.Mock( - description_with_name="s", misconfigured=False) - self.mock_stand.init().more_info.return_value = "standalone" - self.plugins = [ - self.mock_apache, - self.mock_stand, - ] - - def _call(self): - from letsencrypt.display.ops import choose_plugin - return choose_plugin(self.plugins, "Question?") - - @mock.patch("letsencrypt.display.ops.util") - def test_selection(self, mock_util): - mock_util().menu.side_effect = [(display_util.OK, 0), - (display_util.OK, 1)] - self.assertEqual(self.mock_stand, self._call()) - self.assertEqual(mock_util().notification.call_count, 1) - - @mock.patch("letsencrypt.display.ops.util") - def test_more_info(self, mock_util): - mock_util().menu.side_effect = [ - (display_util.HELP, 0), - (display_util.HELP, 1), - (display_util.OK, 1), - ] - - self.assertEqual(self.mock_stand, self._call()) - self.assertEqual(mock_util().notification.call_count, 2) - - @mock.patch("letsencrypt.display.ops.util") - def test_no_choice(self, mock_util): - mock_util().menu.return_value = (display_util.CANCEL, 0) - self.assertTrue(self._call() is None) - - -class PickPluginTest(unittest.TestCase): - """Tests for letsencrypt.display.ops.pick_plugin.""" - - def setUp(self): - self.config = mock.Mock(noninteractive_mode=False) - self.default = None - self.reg = mock.MagicMock() - self.question = "Question?" - self.ifaces = [] - - def _call(self): - from letsencrypt.plugins.selection import pick_plugin - return pick_plugin(self.config, self.default, self.reg, - self.question, self.ifaces) - - def test_default_provided(self): - self.default = "foo" - self._call() - self.assertEqual(1, self.reg.filter.call_count) - - def test_no_default(self): - self._call() - self.assertEqual(1, self.reg.visible().ifaces.call_count) - - def test_no_candidate(self): - self.assertTrue(self._call() is None) - - def test_single(self): - plugin_ep = mock.MagicMock() - plugin_ep.init.return_value = "foo" - plugin_ep.misconfigured = False - - self.reg.visible().ifaces().verify().available.return_value = { - "bar": plugin_ep} - self.assertEqual("foo", self._call()) - - def test_single_misconfigured(self): - plugin_ep = mock.MagicMock() - plugin_ep.init.return_value = "foo" - plugin_ep.misconfigured = True - - self.reg.visible().ifaces().verify().available.return_value = { - "bar": plugin_ep} - self.assertTrue(self._call() is None) - - def test_multiple(self): - plugin_ep = mock.MagicMock() - plugin_ep.init.return_value = "foo" - self.reg.visible().ifaces().verify().available.return_value = { - "bar": plugin_ep, - "baz": plugin_ep, - } - with mock.patch("letsencrypt.display.ops.choose_plugin") as mock_choose: - mock_choose.return_value = plugin_ep - self.assertEqual("foo", self._call()) - mock_choose.assert_called_once_with( - [plugin_ep, plugin_ep], self.question) - - def test_choose_plugin_none(self): - self.reg.visible().ifaces().verify().available.return_value = { - "bar": None, - "baz": None, - } - - with mock.patch("letsencrypt.display.ops.choose_plugin") as mock_choose: - mock_choose.return_value = None - self.assertTrue(self._call() is None) - - -class ConveniencePickPluginTest(unittest.TestCase): - """Tests for letsencrypt.display.ops.pick_*.""" - - def _test(self, fun, ifaces): - config = mock.Mock() - default = mock.Mock() - plugins = mock.Mock() - - with mock.patch("letsencrypt.display.ops.pick_plugin") as mock_p: - mock_p.return_value = "foo" - self.assertEqual("foo", fun(config, default, plugins, "Question?")) - mock_p.assert_called_once_with( - config, default, plugins, "Question?", ifaces) - - def test_authenticator(self): - from letsencrypt.plugins.selection import pick_authenticator - self._test(pick_authenticator, (interfaces.IAuthenticator,)) - - def test_installer(self): - from letsencrypt.plugins.selection import pick_installer - self._test(pick_installer, (interfaces.IInstaller,)) - - def test_configurator(self): - from letsencrypt.plugins.selection import pick_configurator - self._test(pick_configurator, - (interfaces.IAuthenticator, interfaces.IInstaller)) - - class GetEmailTest(unittest.TestCase): """Tests for letsencrypt.display.ops.get_email.""" From 6e9d2b7116c0dcc6cb327c7fd3180b0bf78dac47 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 11 Mar 2016 15:59:11 -0800 Subject: [PATCH 08/34] Adjust mockery... --- letsencrypt/cli.py | 5 ++--- letsencrypt/main.py | 8 ++++---- letsencrypt/tests/cli_test.py | 10 +++++----- letsencrypt/tests/display/ops_test.py | 24 ++++++++++++------------ 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index cb6f66be9..1922cf73b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -22,8 +22,7 @@ from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt.plugins import disco as plugins_disco -from letsencrypt.plugins.selection import cli_plugin_requests - +import letsencrypt.plugins.selection as plugin_selection logger = logging.getLogger(__name__) @@ -118,7 +117,7 @@ def set_by_cli(var): detector = set_by_cli.detector = prepare_and_parse_args( plugins, reconstructed_args, detect_defaults=True) # propagate plugin requests: eg --standalone modifies config.authenticator - auth, inst = cli_plugin_requests(detector) + auth, inst = plugin_selection.cli_plugin_requests(detector) detector.authenticator = auth if auth else "" detector.installer = inst if inst else "" logger.debug("Default Detector is %r", detector) diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 153e118a4..d9e1456ad 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -25,7 +25,7 @@ from letsencrypt import storage from letsencrypt.display import util as display_util, ops as display_ops from letsencrypt.plugins import disco as plugins_disco -from letsencrypt.plugins.selection import choose_configurator_plugins +from letsencrypt.plugins import selection as ps import traceback import logging.handlers @@ -405,7 +405,7 @@ def install(config, plugins): # this function ... try: - installer, _ = choose_configurator_plugins(config, plugins, "install") + installer, _ = ps.choose_configurator_plugins(config, plugins, "install") except errors.PluginSelectionError as e: return e.message @@ -480,7 +480,7 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals # TODO: Make run as close to auth + install as possible # Possible difficulties: config.csr was hacked into auth try: - installer, authenticator = choose_configurator_plugins(config, plugins, "run") + installer, authenticator = ps.choose_configurator_plugins(config, plugins, "run") except errors.PluginSelectionError as e: return e.message @@ -510,7 +510,7 @@ def obtain_cert(config, plugins, lineage=None): # pylint: disable=too-many-locals try: # installers are used in auth mode to determine domain names - installer, authenticator = choose_configurator_plugins(config, plugins, "certonly") + installer, authenticator = ps.choose_configurator_plugins(config, plugins, "certonly") except errors.PluginSelectionError as e: logger.info("Could not choose appropriate plugin: %s", e) raise diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index f1f539016..918addcf8 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -201,12 +201,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(args.chain_path, os.path.abspath(chain)) self.assertEqual(args.fullchain_path, os.path.abspath(fullchain)) - @mock.patch('letsencrypt.main.cli.record_chosen_plugins') - @mock.patch('letsencrypt.main.cli.display_ops') - def test_installer_selection(self, mock_display_ops, _rec): + @mock.patch('letsencrypt.main.ps.record_chosen_plugins') + @mock.patch('letsencrypt.main.ps.pick_installer') + def test_installer_selection(self, mock_pick_installer, _rec): self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', '--key-path', 'key', '--chain-path', 'chain']) - self.assertEqual(mock_display_ops.pick_installer.call_count, 1) + self.assertEqual(mock_pick_installer.call_count, 1) @mock.patch('letsencrypt.le_util.exe_exists') def test_configurator_selection(self, mock_exe_exists): @@ -493,7 +493,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._webroot_map_test(simple_map, "/tmp2", "eg2.com,eg.com", expected_map, domains) # test inclusion of interactively specified domains in the webroot map - with mock.patch('letsencrypt.cli.display_ops.choose_names') as mock_choose: + with mock.patch('letsencrypt.display.ops.choose_names') as mock_choose: mock_choose.return_value = domains expected_map["eg2.com"] = "/tmp" self._webroot_map_test(None, "/tmp", None, expected_map, domains) diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 8a52c872b..0dacdfea8 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -101,17 +101,17 @@ class ChooseAccountTest(unittest.TestCase): from letsencrypt.display import ops return ops.choose_account(accounts) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_one(self, mock_util): mock_util().menu.return_value = (display_util.OK, 0) self.assertEqual(self._call([self.acc1]), self.acc1) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_two(self, mock_util): mock_util().menu.return_value = (display_util.OK, 1) self.assertEqual(self._call([self.acc1, self.acc2]), self.acc2) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_cancel(self, mock_util): mock_util().menu.return_value = (display_util.CANCEL, 1) self.assertTrue(self._call([self.acc1, self.acc2]) is None) @@ -199,12 +199,12 @@ class ChooseNamesTest(unittest.TestCase): self._call(None) self.assertEqual(mock_manual.call_count, 1) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_no_installer_cancel(self, mock_util): mock_util().input.return_value = (display_util.CANCEL, []) self.assertEqual(self._call(None), []) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_no_names_choose(self, mock_util): self.mock_install().get_all_names.return_value = set() mock_util().yesno.return_value = True @@ -215,14 +215,14 @@ class ChooseNamesTest(unittest.TestCase): self.assertEqual(mock_util().input.call_count, 1) self.assertEqual(actual_doms, [domain]) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_no_names_quit(self, mock_util): self.mock_install().get_all_names.return_value = set() mock_util().yesno.return_value = False self.assertEqual(self._call(self.mock_install), []) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_filter_names_valid_return(self, mock_util): self.mock_install.get_all_names.return_value = set(["example.com"]) mock_util().checklist.return_value = (display_util.OK, ["example.com"]) @@ -231,14 +231,14 @@ class ChooseNamesTest(unittest.TestCase): self.assertEqual(names, ["example.com"]) self.assertEqual(mock_util().checklist.call_count, 1) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_filter_names_nothing_selected(self, mock_util): self.mock_install.get_all_names.return_value = set(["example.com"]) mock_util().checklist.return_value = (display_util.OK, []) self.assertEqual(self._call(self.mock_install), []) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_filter_names_cancel(self, mock_util): self.mock_install.get_all_names.return_value = set(["example.com"]) mock_util().checklist.return_value = ( @@ -257,7 +257,7 @@ class ChooseNamesTest(unittest.TestCase): self.assertEqual(get_valid_domains(all_invalid), []) self.assertEqual(len(get_valid_domains(two_valid)), 2) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_choose_manually(self, mock_util): from letsencrypt.display.ops import _choose_names_manually # No retry @@ -305,7 +305,7 @@ class SuccessInstallationTest(unittest.TestCase): from letsencrypt.display.ops import success_installation success_installation(names) - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_success_installation(self, mock_util): mock_util().notification.return_value = None names = ["example.com", "abc.com"] @@ -327,7 +327,7 @@ class SuccessRenewalTest(unittest.TestCase): from letsencrypt.display.ops import success_renewal success_renewal(names, "renew") - @mock.patch("letsencrypt.display.ops.util") + @mock.patch("letsencrypt.display.ops.z_util") def test_success_renewal(self, mock_util): mock_util().notification.return_value = None names = ["example.com", "abc.com"] From fcf1ea32d89c942dc65fb8e877c9d577dd63ecb7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 11 Mar 2016 16:01:52 -0800 Subject: [PATCH 09/34] fixup --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 1922cf73b..f0ea7153b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -22,7 +22,7 @@ from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt.plugins import disco as plugins_disco -import letsencrypt.plugins.selection as plugin_selection +import letsencrypt.plugins.selection as plugin_selection logger = logging.getLogger(__name__) From aac692f8ca812e62ba599ddddfe67eff23786e83 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 11 Mar 2016 17:27:10 -0800 Subject: [PATCH 10/34] Mock-picking fix --- letsencrypt/tests/client_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 8be2178b9..4331be59d 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -422,9 +422,8 @@ class RollbackTest(unittest.TestCase): @classmethod def _call(cls, checkpoints, side_effect): from letsencrypt.client import rollback - with mock.patch("letsencrypt.client" - ".display_ops.pick_installer") as mock_pick_installer: - mock_pick_installer.side_effect = side_effect + with mock.patch("letsencrypt.client.plugin_selection.pick_installer") as mpi + mpi.side_effect = side_effect rollback(None, checkpoints, {}, mock.MagicMock()) def test_no_problems(self): From b888a2bd526d758d5c5f2d23c16870db1212b186 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 11 Mar 2016 17:30:02 -0800 Subject: [PATCH 11/34] Fixup --- letsencrypt/tests/client_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 4331be59d..f14a52407 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -422,7 +422,7 @@ class RollbackTest(unittest.TestCase): @classmethod def _call(cls, checkpoints, side_effect): from letsencrypt.client import rollback - with mock.patch("letsencrypt.client.plugin_selection.pick_installer") as mpi + with mock.patch("letsencrypt.client.plugin_selection.pick_installer") as mpi: mpi.side_effect = side_effect rollback(None, checkpoints, {}, mock.MagicMock()) From d508a47e516d85450a47d3b763e62e3ee1692ee4 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 18:08:53 +0200 Subject: [PATCH 12/34] Added IPv6 address to Apache test data vhost --- .../two_vhost_80/apache2/sites-available/000-default.conf | 2 +- letsencrypt-apache/letsencrypt_apache/tests/util.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf index c759768c5..2bd4e1fe9 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf @@ -1,4 +1,4 @@ - + ServerName ip-172-30-0-17 ServerAdmin webmaster@localhost diff --git a/letsencrypt-apache/letsencrypt_apache/tests/util.py b/letsencrypt-apache/letsencrypt_apache/tests/util.py index fb1e1442d..5bcb6aca6 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/util.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/util.py @@ -133,8 +133,9 @@ def get_vh_truth(temp_dir, config_name): obj.VirtualHost( os.path.join(prefix, "000-default.conf"), os.path.join(aug_pre, "000-default.conf/VirtualHost"), - set([obj.Addr.fromstring("*:80")]), False, True, - "ip-172-30-0-17"), + set([obj.Addr.fromstring("*:80"), + obj.Addr.fromstring("[::]:80")]), + False, True, "ip-172-30-0-17"), obj.VirtualHost( os.path.join(prefix, "letsencrypt.conf"), os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"), From 8fbe7de625eea585384f45be3810363cd099207e Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 18:09:43 +0200 Subject: [PATCH 13/34] Added IPv6 normalization and comparison to Addr object --- letsencrypt/plugins/common.py | 48 +++++++++++++++++++++++++++--- letsencrypt/plugins/common_test.py | 8 +++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index b72c57c7b..0b8ab635a 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -104,8 +104,9 @@ class Addr(object): :param str port: port number or \*, or "" """ - def __init__(self, tup): + def __init__(self, tup, ipv6=False): self.tup = tup + self.ipv6 = ipv6 @classmethod def fromstring(cls, str_addr): @@ -117,7 +118,7 @@ class Addr(object): port = '' if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':': port = str_addr[endIndex + 2:] - return cls((host, port)) + return cls((host, port), True) else: tup = str_addr.partition(':') return cls((tup[0], tup[2])) @@ -129,7 +130,15 @@ class Addr(object): def __eq__(self, other): if isinstance(other, self.__class__): - return self.tup == other.tup + if self.ipv6: + # import ipdb;ipdb.set_trace() + return (other.ipv6 and + self._normalize_ipv6(self.tup[0]) == + self._normalize_ipv6(other.tup[0]) and + self.tup[1] == other.tup[1]) + else: + return self.tup == other.tup + return False def __hash__(self): @@ -145,7 +154,38 @@ class Addr(object): def get_addr_obj(self, port): """Return new address object with same addr and new port.""" - return self.__class__((self.tup[0], port)) + return self.__class__((self.tup[0], port), self.ipv6) + + def _normalize_ipv6(self, addr): + """Return IPv6 address in normalized form, helper function""" + addr = addr.lstrip("[") + addr = addr.rstrip("]") + return self._explode_ipv6(addr) + + def get_ipv6_exploded(self): + """Return IPv6 in normalized form""" + if self.ipv6: + return ":".join(self._normalize_ipv6(self.tup[0])) + return "" + + def _explode_ipv6(self, addr): + """Explode IPv6 address for comparison""" + result = ['0', '0', '0', '0', '0', '0', '0', '0'] + addr_list = addr.split(":") + append_to_end = False + for i in range(0, len(addr_list)): + block = addr_list[i] + if len(block) == 0: + append_to_end = True + continue + elif len(block) > 1: + # remove trailing zeros + block = block.lstrip("0") + if not append_to_end: + result[i] = str(block) + else: + result[i-len(addr_list)] = str(block) + return result class TLSSNI01(object): diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 5fdd57c5f..9021e7e42 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -98,6 +98,10 @@ class AddrTest(unittest.TestCase): self.assertEqual(self.addr5.get_port(), "*") self.assertEqual(self.addr6.get_addr(), "[fe00::1]") self.assertEqual(self.addr6.get_port(), "80") + self.assertEqual(self.addr6.get_ipv6_exploded(), + "fe00:0:0:0:0:0:0:1") + self.assertEqual(self.addr1.get_ipv6_exploded(), + "") def test_str(self): self.assertEqual(str(self.addr1), "192.168.1.1") @@ -123,6 +127,10 @@ class AddrTest(unittest.TestCase): self.assertEqual(self.addr4, self.addr4.get_addr_obj("")) self.assertNotEqual(self.addr4, self.addr5) self.assertFalse(self.addr4 == 3333) + from letsencrypt.plugins.common import Addr + self.assertEqual(self.addr4, Addr.fromstring("[fe00:0:0::1]")) + self.assertEqual(self.addr4, Addr.fromstring("[fe00:0::0:0:1]")) + def test_set_inclusion(self): from letsencrypt.plugins.common import Addr From 092a1ee0fb8385e247ae45f785112a317183eb16 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 18:37:57 +0200 Subject: [PATCH 14/34] Made apache configs a bit more generic for tests --- .../apache-conf-files/passing/ipv6-1143.conf | 12 +++++----- .../apache-conf-files/passing/ipv6-1143b.conf | 18 +++++++------- .../apache-conf-files/passing/ipv6-1143c.conf | 12 +++++----- .../apache-conf-files/passing/ipv6-1143d.conf | 24 ++++++++++++++----- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf index ab4ed412e..22b39c9f2 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf @@ -1,9 +1,9 @@ - -DocumentRoot /xxxx/ -ServerName noodles.net.nz -ServerAlias www.noodles.net.nz -CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined - + +DocumentRoot /var/www/html/ +ServerName example.com +ServerAlias www.example.com +CustomLog ${APACHE_LOG_DIR}/example.log combined + AllowOverride All diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf index 25655a07c..4df497ab2 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf @@ -1,10 +1,9 @@ - - -DocumentRoot /xxxx/ -ServerName noodles.net.nz -ServerAlias www.noodles.net.nz -CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined - + +DocumentRoot /var/www/html/ +ServerName example.com +ServerAlias www.example.com +CustomLog ${APACHE_LOG_DIR}/example.log combined + AllowOverride All @@ -14,8 +13,9 @@ CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined SSLProtocol all -SSLv2 -SSLv3 SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS" - SSLCertificateFile /xxxx/noodles.net.nz.crt - SSLCertificateKeyFile /xxxx/noodles.net.nz.key + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + Header set Strict-Transport-Security "max-age=31536000; preload" diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf index f75dd7850..40670b336 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf @@ -1,9 +1,9 @@ - -DocumentRoot /xxxx/ -ServerName noodles.net.nz -ServerAlias www.noodles.net.nz -CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined - + +DocumentRoot /var/www/html/ +ServerName example.com +ServerAlias www.example.com +CustomLog ${APACHE_LOG_DIR}/example.log combined + AllowOverride All diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf index f16b412da..813c41b62 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf @@ -1,9 +1,21 @@ - -DocumentRoot /xxxx/ -ServerName noodles.net.nz -ServerAlias www.noodles.net.nz -CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined - + +DocumentRoot /var/www/html/ +ServerName example.com +ServerAlias www.example.com +CustomLog ${APACHE_LOG_DIR}/example.log combined + AllowOverride All + + SSLEngine on + + SSLHonorCipherOrder On + SSLProtocol all -SSLv2 -SSLv3 + SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS" + + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + + + Header set Strict-Transport-Security "max-age=31536000; preload" From 9f1504eecd54bad05ca60617a9d4f67a0859bfac Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 20:34:54 +0200 Subject: [PATCH 15/34] Trying to please Travis --- .../tests/apache-conf-files/passing/ipv6-1143.conf | 4 ++-- .../tests/apache-conf-files/passing/ipv6-1143b.conf | 7 ++----- .../tests/apache-conf-files/passing/ipv6-1143c.conf | 4 ++-- .../tests/apache-conf-files/passing/ipv6-1143d.conf | 7 ++----- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf index 22b39c9f2..ad988dc05 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf @@ -1,9 +1,9 @@ -DocumentRoot /var/www/html/ +DocumentRoot /tmp ServerName example.com ServerAlias www.example.com CustomLog ${APACHE_LOG_DIR}/example.log combined - + AllowOverride All diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf index 4df497ab2..e2b4fd3da 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf @@ -1,9 +1,9 @@ -DocumentRoot /var/www/html/ +DocumentRoot /tmp ServerName example.com ServerAlias www.example.com CustomLog ${APACHE_LOG_DIR}/example.log combined - + AllowOverride All @@ -15,7 +15,4 @@ CustomLog ${APACHE_LOG_DIR}/example.log combined SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key - - - Header set Strict-Transport-Security "max-age=31536000; preload" diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf index 40670b336..f2d2ecbea 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf @@ -1,9 +1,9 @@ -DocumentRoot /var/www/html/ +DocumentRoot /tmp ServerName example.com ServerAlias www.example.com CustomLog ${APACHE_LOG_DIR}/example.log combined - + AllowOverride All diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf index 813c41b62..f5b7a2b45 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf @@ -1,9 +1,9 @@ -DocumentRoot /var/www/html/ +DocumentRoot /tmp ServerName example.com ServerAlias www.example.com CustomLog ${APACHE_LOG_DIR}/example.log combined - + AllowOverride All @@ -15,7 +15,4 @@ CustomLog ${APACHE_LOG_DIR}/example.log combined SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key - - - Header set Strict-Transport-Security "max-age=31536000; preload" From 7675d50f4cf2addeb1995db8fcbba0cc53802609 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 22:29:01 +0200 Subject: [PATCH 16/34] Cleaning up debugging leftovers --- letsencrypt/plugins/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 0b8ab635a..9b3bee7ef 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -131,7 +131,6 @@ class Addr(object): def __eq__(self, other): if isinstance(other, self.__class__): if self.ipv6: - # import ipdb;ipdb.set_trace() return (other.ipv6 and self._normalize_ipv6(self.tup[0]) == self._normalize_ipv6(other.tup[0]) and From 432a85cd18f4b876434fd75d4a65f09222c7029a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:19:57 -0700 Subject: [PATCH 17/34] Add Ncurses directory_select --- letsencrypt/display/util.py | 24 ++++++++++++++++++++++++ letsencrypt/tests/display/util_test.py | 5 +++++ 2 files changed, 29 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 84049c47c..3913f8bf1 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -11,6 +11,13 @@ from letsencrypt import errors WIDTH = 72 HEIGHT = 20 +DSELECT_HELP = ( + "Use the arrow keys or tab to move between window elements. Space can be " + "used to complete the input path with the selected element in the " + "directory window. Pressing enter will select the currently highlighted " + "button.") +"""Help text on how to use dialog's dselect.""" + # Display exit codes OK = "ok" """Display exit code indicating user acceptance.""" @@ -21,6 +28,7 @@ CANCEL = "cancel" HELP = "help" """Display exit code when for when the user requests more help.""" + def _wrap_lines(msg): """Format lines nicely to 80 chars. @@ -36,6 +44,7 @@ def _wrap_lines(msg): fixed_l.append(textwrap.fill(line, 80)) return os.linesep.join(fixed_l) + @zope.interface.implementer(interfaces.IDisplay) class NcursesDisplay(object): """Ncurses-based display.""" @@ -174,6 +183,21 @@ class NcursesDisplay(object): return self.dialog.checklist( message, width=self.width, height=self.height, choices=choices) + def directory_select(self, message, **unused_kwargs): + """Display a directory selection screen. + + :param str message: prompt to give the user + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + root_directory = os.path.abspath(os.sep) + return self.dialog.dselect( + filepath=root_directory, width=self.width, + height=self.height, help_button=True, title=message) + @zope.interface.implementer(interfaces.IDisplay) class FileDisplay(object): diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index a16eb544e..45025d7a0 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -123,6 +123,11 @@ class NcursesDisplayTest(unittest.TestCase): "message", width=display_util.WIDTH, height=display_util.HEIGHT, choices=choices) + @mock.patch("letsencrypt.display.util.dialog.Dialog.dselect") + def test_directory_select(self, mock_dselect): + self.displayer.directory_select("message") + self.assertEqual(mock_dselect.call_count, 1) + class FileOutputDisplayTest(unittest.TestCase): """Test stdout display. From 08232eef43680e189aaf2776f0953c2a22bd5dfe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:22:15 -0700 Subject: [PATCH 18/34] display.util pep8 cleanup --- letsencrypt/display/util.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 3913f8bf1..05e280118 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -127,7 +127,6 @@ class NcursesDisplay(object): return code, int(index) - 1 - def input(self, message, **unused_kwargs): """Display an input box to the user. @@ -141,11 +140,10 @@ class NcursesDisplay(object): """ sections = message.split("\n") # each section takes at least one line, plus extras if it's longer than self.width - wordlines = [1 + (len(section)/self.width) for section in sections] + wordlines = [1 + (len(section) / self.width) for section in sections] height = 6 + sum(wordlines) + len(sections) return self.dialog.inputbox(message, width=self.width, height=height) - def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): """Display a Yes/No dialog box. @@ -397,7 +395,6 @@ class FileDisplay(object): self.outfile.write(side_frame) - def _get_valid_int_ans(self, max_): """Get a numerical selection. @@ -433,6 +430,7 @@ class FileDisplay(object): return OK, selection + @zope.interface.implementer(interfaces.IDisplay) class NoninteractiveDisplay(object): """An iDisplay implementation that never asks for interactive user input""" @@ -507,7 +505,6 @@ class NoninteractiveDisplay(object): else: return OK, default - def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None): # pylint: disable=unused-argument """Decide Yes or No, without asking anybody From 95a4a2ca6098f23322f7ed8e30c218d198e52903 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:23:21 -0700 Subject: [PATCH 19/34] display.util_test cleanup --- letsencrypt/tests/display/util_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index 45025d7a0..adec265dc 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -285,6 +285,7 @@ class FileOutputDisplayTest(unittest.TestCase): self.displayer._get_valid_int_ans(3), (display_util.CANCEL, -1)) + class NoninteractiveDisplayTest(unittest.TestCase): """Test non-interactive display. From 0fd453fd5e16c08bed271010539be61d874aa131 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 22 Mar 2016 10:23:14 +0200 Subject: [PATCH 20/34] Added code comments to clarify inner workings --- letsencrypt/plugins/common.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 9b3bee7ef..79c95e149 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -131,6 +131,8 @@ class Addr(object): def __eq__(self, other): if isinstance(other, self.__class__): if self.ipv6: + # compare normalized to take different + # styles of representation into account return (other.ipv6 and self._normalize_ipv6(self.tup[0]) == self._normalize_ipv6(other.tup[0]) and @@ -175,14 +177,17 @@ class Addr(object): for i in range(0, len(addr_list)): block = addr_list[i] if len(block) == 0: + # encountered ::, so rest of the blocks should be + # appended to the end append_to_end = True continue elif len(block) > 1: - # remove trailing zeros + # remove leading zeros block = block.lstrip("0") if not append_to_end: result[i] = str(block) else: + # count the location from the end using negative indices result[i-len(addr_list)] = str(block) return result From 74a7d2bed90a3ecadf73f723f913c6a6e4f0553b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 22 Mar 2016 18:07:51 -0700 Subject: [PATCH 21/34] Added completer.py and tests for FileDisplay --- letsencrypt/display/completer.py | 51 ++++++++++++++++ letsencrypt/tests/display/completer_test.py | 64 +++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 letsencrypt/display/completer.py create mode 100644 letsencrypt/tests/display/completer_test.py diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py new file mode 100644 index 000000000..5258ef52e --- /dev/null +++ b/letsencrypt/display/completer.py @@ -0,0 +1,51 @@ +"""Provides tab autocompletion when prompting users for a path.""" +import glob +import readline + + +class Completer(object): + """Provides tab autocompletion when prompting users for a path. + + This class is meant to be used with readline to provide tab + autocompletion for users entering paths. The complete method can + be passed to readline.set_completer directly, however, this function + works best as a context manager. For example: + + with Completer(): + raw_input() + + In this example, tab autocompletion will be available during + the call to raw_input above, however, readline will be restored to + its previous state when exiting the body of the with statement. + + """ + + def __init__(self): + self._completer = self._delims = self._iter = None + + def complete(self, text, state): + """Provides path autocompletion for use with readline. + + :param str text: text to offer completions for + :param int state: which completion to return + + :returns: possible completion for text or ``None`` if all + completions have been returned + :rtype: str + + """ + if state == 0: + self._iter = glob.iglob(text + '*') + return next(self._iter, None) + + def __enter__(self): + self._completer = readline.get_completer() + self._delims = readline.get_completer_delims() + + readline.set_completer(self.complete) + readline.set_completer_delims(' \t\n;') + readline.parse_and_bind('tab: complete') + + def __exit__(self, unused_type, unused_value, unused_traceback): + readline.set_completer_delims(self._delims) + readline.set_completer(self._completer) diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py new file mode 100644 index 000000000..dbba15106 --- /dev/null +++ b/letsencrypt/tests/display/completer_test.py @@ -0,0 +1,64 @@ +"""Test letsencrypt.display.completer.""" +import os +import readline +import shutil +import string +import tempfile +import unittest + + +class CompleterTest(unittest.TestCase): + """Test letsencrypt.display.completer.Completer.""" + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + + # directories must end with os.sep for completer to + # search inside the directory for possible completions + if self.temp_dir[-1] != os.sep: + self.temp_dir += os.sep + + self.paths = [] + # create some files and directories in temp_dir + for c in string.ascii_lowercase: + path = os.path.join(self.temp_dir, c) + self.paths.append(path) + if ord(c) % 2: + os.mkdir(path) + else: + with open(path, 'w'): + pass + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + def test_context_manager(self): + from letsencrypt.display import completer + + original_completer = readline.get_completer() + original_delims = readline.get_completer_delims() + + with completer.Completer(): + pass + + self.assertEqual(readline.get_completer(), original_completer) + self.assertEqual(readline.get_completer_delims(), original_delims) + + def test_complete(self): + from letsencrypt.display import completer + + my_completer = completer.Completer() + num_paths = len(self.paths) + + for i in range(num_paths): + completion = my_completer.complete(self.temp_dir, i) + self.assertTrue(completion in self.paths) + self.paths.remove(completion) + + self.assertFalse(self.paths) + completion = my_completer.complete(self.temp_dir, num_paths) + self.assertEqual(completion, None) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From 49fefb08dd126f881d33f8c32ec5caf78acf0e2c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 10:35:38 -0700 Subject: [PATCH 22/34] autocomplete -> complete --- letsencrypt/display/completer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 5258ef52e..71e72b942 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,22 +1,22 @@ -"""Provides tab autocompletion when prompting users for a path.""" +"""Provides tab completion when prompting users for a path.""" import glob import readline class Completer(object): - """Provides tab autocompletion when prompting users for a path. + """Provides tab completion when prompting users for a path. This class is meant to be used with readline to provide tab - autocompletion for users entering paths. The complete method can - be passed to readline.set_completer directly, however, this function + completion for users entering paths. The complete method can be + passed to readline.set_completer directly, however, this function works best as a context manager. For example: with Completer(): raw_input() - In this example, tab autocompletion will be available during - the call to raw_input above, however, readline will be restored to - its previous state when exiting the body of the with statement. + In this example, tab completion will be available during the call to + raw_input above, however, readline will be restored to its previous + state when exiting the body of the with statement. """ @@ -24,7 +24,7 @@ class Completer(object): self._completer = self._delims = self._iter = None def complete(self, text, state): - """Provides path autocompletion for use with readline. + """Provides path completion for use with readline. :param str text: text to offer completions for :param int state: which completion to return From f9ac7d789b898353a79e74a07d93f71e63d362ee Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 11:15:37 -0700 Subject: [PATCH 23/34] Add support libedit readline --- letsencrypt/display/completer.py | 8 +++- letsencrypt/tests/display/completer_test.py | 52 ++++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 71e72b942..6b07a2e47 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -44,7 +44,13 @@ class Completer(object): readline.set_completer(self.complete) readline.set_completer_delims(' \t\n;') - readline.parse_and_bind('tab: complete') + + # readline can be implemented using GNU readline or libedit + # which have different configuration syntax + if 'libedit' in readline.__doc__: + readline.parse_and_bind('bind ^I rl_complete') + else: + readline.parse_and_bind('tab: complete') def __exit__(self, unused_type, unused_value, unused_traceback): readline.set_completer_delims(self._delims) diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py index dbba15106..a77d3c842 100644 --- a/letsencrypt/tests/display/completer_test.py +++ b/letsencrypt/tests/display/completer_test.py @@ -6,6 +6,8 @@ import string import tempfile import unittest +import mock + class CompleterTest(unittest.TestCase): """Test letsencrypt.display.completer.Completer.""" @@ -32,18 +34,6 @@ class CompleterTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.temp_dir) - def test_context_manager(self): - from letsencrypt.display import completer - - original_completer = readline.get_completer() - original_delims = readline.get_completer_delims() - - with completer.Completer(): - pass - - self.assertEqual(readline.get_completer(), original_completer) - self.assertEqual(readline.get_completer_delims(), original_delims) - def test_complete(self): from letsencrypt.display import completer @@ -59,6 +49,44 @@ class CompleterTest(unittest.TestCase): completion = my_completer.complete(self.temp_dir, num_paths) self.assertEqual(completion, None) + def test_context_manager(self): + from letsencrypt.display import completer + + original_completer = readline.get_completer() + original_delims = readline.get_completer_delims() + + with completer.Completer(): + pass + + self.assertEqual(readline.get_completer(), original_completer) + self.assertEqual(readline.get_completer_delims(), original_delims) + + @mock.patch('letsencrypt.display.completer.readline', autospec=True) + def test_context_manager_libedit(self, mock_readline): + mock_readline.__doc__ = 'libedit' + self._test_mocked_readline(mock_readline) + + @mock.patch('letsencrypt.display.completer.readline', autospec=True) + def test_context_manager_readline(self, mock_readline): + mock_readline.__doc__ = 'GNU readline' + self._test_mocked_readline(mock_readline) + + def _test_mocked_readline(self, mock_readline): + from letsencrypt.display import completer + + mock_readline.parse_and_bind.side_effect = enable_tab_completion + + with completer.Completer(): + pass + + self.assertTrue(mock_readline.parse_and_bind.called) + + +def enable_tab_completion(unused_command): + """Enables readline tab completion using the system specific syntax.""" + libedit = 'libedit' in readline.__doc__ + command = 'bind ^I rl_complete' if libedit else 'tab: complete' + readline.parse_and_bind(command) if __name__ == "__main__": unittest.main() # pragma: no cover From 0c0687ca68ae4b9c4fa2585bfd1a974d121bed30 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 11:27:20 -0700 Subject: [PATCH 24/34] Add dummy_readline module --- letsencrypt/display/dummy_readline.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 letsencrypt/display/dummy_readline.py diff --git a/letsencrypt/display/dummy_readline.py b/letsencrypt/display/dummy_readline.py new file mode 100644 index 000000000..fb3d807bb --- /dev/null +++ b/letsencrypt/display/dummy_readline.py @@ -0,0 +1,21 @@ +"""A dummy module with no effect for use on systems without readline.""" + + +def get_completer(): + """An empty implementation of readline.get_completer.""" + + +def get_completer_delims(): + """An empty implementation of readline.get_completer_delims.""" + + +def parse_and_bind(unused_command): + """An empty implementation of readline.parse_and_bind.""" + + +def set_completer(unused_function=None): + """An empty implementation of readline.set_completer.""" + + +def set_completer_delims(unused_delims): + """An empty implementation of readline.set_completer_delims.""" From 7820c687f16550c7a406209918092cfbbd9c53de Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 13:33:07 -0700 Subject: [PATCH 25/34] Use dummy_readline to prevent ImportErrors from readline breaking LE --- letsencrypt/display/completer.py | 6 +++++- letsencrypt/tests/display/completer_test.py | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 6b07a2e47..83dafa25d 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,6 +1,10 @@ """Provides tab completion when prompting users for a path.""" import glob -import readline +# readline module is not available on all systems +try: + import readline +except ImportError: + import letsencrypt.display.dummy_readline as readline class Completer(object): diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py index a77d3c842..3c181c925 100644 --- a/letsencrypt/tests/display/completer_test.py +++ b/letsencrypt/tests/display/completer_test.py @@ -3,10 +3,12 @@ import os import readline import shutil import string +import sys import tempfile import unittest import mock +from six.moves import reload_module # pylint: disable=import-error class CompleterTest(unittest.TestCase): @@ -36,7 +38,6 @@ class CompleterTest(unittest.TestCase): def test_complete(self): from letsencrypt.display import completer - my_completer = completer.Completer() num_paths = len(self.paths) @@ -49,8 +50,17 @@ class CompleterTest(unittest.TestCase): completion = my_completer.complete(self.temp_dir, num_paths) self.assertEqual(completion, None) - def test_context_manager(self): + def test_import_error(self): + original_readline = sys.modules['readline'] + sys.modules['readline'] = None + + self.test_context_manager_with_unmocked_readline() + + sys.modules['readline'] = original_readline + + def test_context_manager_with_unmocked_readline(self): from letsencrypt.display import completer + reload_module(completer) original_completer = readline.get_completer() original_delims = readline.get_completer_delims() @@ -64,14 +74,14 @@ class CompleterTest(unittest.TestCase): @mock.patch('letsencrypt.display.completer.readline', autospec=True) def test_context_manager_libedit(self, mock_readline): mock_readline.__doc__ = 'libedit' - self._test_mocked_readline(mock_readline) + self._test_context_manager_with_mock_readline(mock_readline) @mock.patch('letsencrypt.display.completer.readline', autospec=True) def test_context_manager_readline(self, mock_readline): mock_readline.__doc__ = 'GNU readline' - self._test_mocked_readline(mock_readline) + self._test_context_manager_with_mock_readline(mock_readline) - def _test_mocked_readline(self, mock_readline): + def _test_context_manager_with_mock_readline(self, mock_readline): from letsencrypt.display import completer mock_readline.parse_and_bind.side_effect = enable_tab_completion From 9d70c4acfbc78fda0a2073f2d157a8e8bd9de84b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:11:36 -0700 Subject: [PATCH 26/34] Add directory_select method to FileDisplay --- letsencrypt/display/util.py | 14 ++++++++++++++ letsencrypt/tests/display/util_test.py | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 05e280118..c94b0c921 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -7,6 +7,7 @@ import zope.interface from letsencrypt import interfaces from letsencrypt import errors +from letsencrypt.display import completer WIDTH = 72 HEIGHT = 20 @@ -339,6 +340,19 @@ class FileDisplay(object): else: return code, [] + def directory_select(self, message, **unused_kwargs): + """Display a directory selection screen. + + :param str message: prompt to give the user + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + with completer.Completer(): + return self.input(message) + def _scrub_checklist_input(self, indices, tags): # pylint: disable=no-self-use """Validate input and transform indices to appropriate tags. diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index adec265dc..32783b3bd 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -232,6 +232,15 @@ class FileOutputDisplayTest(unittest.TestCase): self.displayer._scrub_checklist_input(list_, TAGS)) self.assertEqual(set_tags, exp[i]) + @mock.patch("letsencrypt.display.util.FileDisplay.input") + def test_directory_select(self, mock_input): + message = "msg" + result = (display_util.OK, "/var/www/html",) + mock_input.return_value = result + + self.assertEqual(self.displayer.directory_select(message), result) + mock_input.assert_called_once_with(message) + def test_scrub_checklist_input_invalid(self): # pylint: disable=protected-access indices = [ From 294ea4d1a64be7be394854af177d35c60c2f1184 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:29:02 -0700 Subject: [PATCH 27/34] Add directory_select to NoninteractiveDisplay --- letsencrypt/display/util.py | 17 +++++++++++++++++ letsencrypt/tests/display/util_test.py | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index c94b0c921..005e2ba9c 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -555,6 +555,23 @@ class NoninteractiveDisplay(object): else: return OK, default + def directory_select(self, message, default=None, cli_flag=None): + """Simulate prompting the user for a directory. + + This function returns default if it is not ``None``, otherwise, + an exception is raised. + + :param str message: prompt to give the user + :param default: default value to return (if one exists) + :param str cli_flag: option used to set this value with the CLI + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + return self.input(message, default, cli_flag) + def separate_list_input(input_): """Separate a comma or space separated list. diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index 32783b3bd..bae0d582a 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -335,6 +335,15 @@ class NoninteractiveDisplayTest(unittest.TestCase): self.assertEqual(ret, (display_util.OK, d)) self.assertRaises(errors.MissingCommandlineFlag, self.displayer.checklist, "message", TAGS) + def test_directory_select(self): + default = "/var/www/html" + expected = (display_util.OK, default) + actual = self.displayer.directory_select("msg", default) + self.assertEqual(expected, actual) + + self.assertRaises( + errors.MissingCommandlineFlag, self.displayer.directory_select, "msg") + class SeparateListInputTest(unittest.TestCase): """Test Module functions.""" From 3031968ac5e148a857e057485f31dbbdbdbe0f39 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:32:53 -0700 Subject: [PATCH 28/34] Add directory_select to IDisplay interface --- letsencrypt/interfaces.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 1921b1e54..188a4d9da 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -443,6 +443,20 @@ class IDisplay(zope.interface.Interface): """ + def directory_select(self, message, default=None, cli_flag=None): + """Display a directory selection screen. + + :param str message: prompt to give the user + :param default: the default value to return, if one exists, when + using the NoninteractiveDisplay + :param str cli_flag: option used to set this value with the CLI + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + class IValidator(zope.interface.Interface): """Configuration validator.""" From 7b434c5a88aff2d6102680eb2b118e5a99db3620 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 24 Mar 2016 17:51:48 -0700 Subject: [PATCH 29/34] Address review comments --- letsencrypt/main.py | 8 ++++---- letsencrypt/plugins/selection.py | 4 +--- letsencrypt/tests/cli_test.py | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 15c2de3ee..4e35a5227 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -32,7 +32,7 @@ from letsencrypt import storage from letsencrypt.display import util as display_util, ops as display_ops from letsencrypt.plugins import disco as plugins_disco -from letsencrypt.plugins import selection as ps +from letsencrypt.plugins import selection as plug_sel logger = logging.getLogger(__name__) @@ -407,7 +407,7 @@ def install(config, plugins): # this function ... try: - installer, _ = ps.choose_configurator_plugins(config, plugins, "install") + installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "install") except errors.PluginSelectionError as e: return e.message @@ -482,7 +482,7 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals # TODO: Make run as close to auth + install as possible # Possible difficulties: config.csr was hacked into auth try: - installer, authenticator = ps.choose_configurator_plugins(config, plugins, "run") + installer, authenticator = plug_sel.choose_configurator_plugins(config, plugins, "run") except errors.PluginSelectionError as e: return e.message @@ -512,7 +512,7 @@ def obtain_cert(config, plugins, lineage=None): # pylint: disable=too-many-locals try: # installers are used in auth mode to determine domain names - installer, authenticator = ps.choose_configurator_plugins(config, plugins, "certonly") + installer, authenticator = plug_sel.choose_configurator_plugins(config, plugins, "certonly") except errors.PluginSelectionError as e: logger.info("Could not choose appropriate plugin: %s", e) raise diff --git a/letsencrypt/plugins/selection.py b/letsencrypt/plugins/selection.py index 6f86075cd..057a09768 100644 --- a/letsencrypt/plugins/selection.py +++ b/letsencrypt/plugins/selection.py @@ -132,8 +132,6 @@ def choose_plugin(prepared, question): else: return None -logger = logging.getLogger(__name__) - noninstaller_plugins = ["webroot", "manual", "standalone"] def record_chosen_plugins(config, plugins, auth, inst): @@ -146,7 +144,7 @@ def record_chosen_plugins(config, plugins, auth, inst): def choose_configurator_plugins(config, plugins, verb): """ Figure out which configurator we're going to use, modifies - config.authenticator and config.istaller strings to reflect that choice if + config.authenticator and config.installer strings to reflect that choice if necessary. :raises errors.PluginSelectionError if there was a problem diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index b69716924..04b5a2f3c 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -203,8 +203,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(args.chain_path, os.path.abspath(chain)) self.assertEqual(args.fullchain_path, os.path.abspath(fullchain)) - @mock.patch('letsencrypt.main.ps.record_chosen_plugins') - @mock.patch('letsencrypt.main.ps.pick_installer') + @mock.patch('letsencrypt.main.plug_sel.record_chosen_plugins') + @mock.patch('letsencrypt.main.plug_sel.pick_installer') def test_installer_selection(self, mock_pick_installer, _rec): self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', '--key-path', 'key', '--chain-path', 'chain']) From 2927746c0e8ea8163809d7b3ddd62ea375ab342d Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 25 Mar 2016 17:48:13 +0200 Subject: [PATCH 30/34] Added important check for IPv6 address and tests, improved readability --- letsencrypt/plugins/common.py | 5 ++++- letsencrypt/plugins/common_test.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 707ed7e57..a9410d514 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -118,7 +118,7 @@ class Addr(object): port = '' if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':': port = str_addr[endIndex + 2:] - return cls((host, port), True) + return cls((host, port), ipv6=True) else: tup = str_addr.partition(':') return cls((tup[0], tup[2])) @@ -173,6 +173,9 @@ class Addr(object): """Explode IPv6 address for comparison""" result = ['0', '0', '0', '0', '0', '0', '0', '0'] addr_list = addr.split(":") + if len(addr_list) > len(result): + # too long, truncate + addr_list = addr_list[0:len(result)] append_to_end = False for i in range(0, len(addr_list)): block = addr_list[i] diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 9021e7e42..292248b90 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -84,6 +84,8 @@ class AddrTest(unittest.TestCase): self.addr4 = Addr.fromstring("[fe00::1]") self.addr5 = Addr.fromstring("[fe00::1]:*") self.addr6 = Addr.fromstring("[fe00::1]:80") + self.addr7 = Addr.fromstring("[fe00::1]:5") + self.addr8 = Addr.fromstring("[fe00:1:2:3:4:5:6:7:8:9]:8080") def test_fromstring(self): self.assertEqual(self.addr1.get_addr(), "192.168.1.1") @@ -102,6 +104,9 @@ class AddrTest(unittest.TestCase): "fe00:0:0:0:0:0:0:1") self.assertEqual(self.addr1.get_ipv6_exploded(), "") + self.assertEqual(self.addr7.get_port(), "5") + self.assertEqual(self.addr7.get_ipv6_exploded(), + "fe00:1:2:3:4:5:6:7") def test_str(self): self.assertEqual(str(self.addr1), "192.168.1.1") From f2fc79fc55e92abb2d19c7f1491af8f1aefe64cc Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 25 Mar 2016 19:12:48 +0200 Subject: [PATCH 31/34] Fixed test case --- letsencrypt/plugins/common_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 292248b90..a4292151e 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -105,7 +105,7 @@ class AddrTest(unittest.TestCase): self.assertEqual(self.addr1.get_ipv6_exploded(), "") self.assertEqual(self.addr7.get_port(), "5") - self.assertEqual(self.addr7.get_ipv6_exploded(), + self.assertEqual(self.addr8.get_ipv6_exploded(), "fe00:1:2:3:4:5:6:7") def test_str(self): From f520ca25565429141d44ed28e3b1fda705ea9682 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 25 Mar 2016 12:52:37 -0700 Subject: [PATCH 32/34] Address @schoen's review comments --- letsencrypt/display/completer.py | 18 +++++++++--------- letsencrypt/display/util.py | 6 ++++-- letsencrypt/interfaces.py | 4 +++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 83dafa25d..fed476bb3 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,4 +1,4 @@ -"""Provides tab completion when prompting users for a path.""" +"""Provides Tab completion when prompting users for a path.""" import glob # readline module is not available on all systems try: @@ -8,9 +8,9 @@ except ImportError: class Completer(object): - """Provides tab completion when prompting users for a path. + """Provides Tab completion when prompting users for a path. - This class is meant to be used with readline to provide tab + This class is meant to be used with readline to provide Tab completion for users entering paths. The complete method can be passed to readline.set_completer directly, however, this function works best as a context manager. For example: @@ -18,14 +18,14 @@ class Completer(object): with Completer(): raw_input() - In this example, tab completion will be available during the call to + In this example, Tab completion will be available during the call to raw_input above, however, readline will be restored to its previous state when exiting the body of the with statement. """ def __init__(self): - self._completer = self._delims = self._iter = None + self._iter = self._original_completer = self._original_delims = None def complete(self, text, state): """Provides path completion for use with readline. @@ -43,8 +43,8 @@ class Completer(object): return next(self._iter, None) def __enter__(self): - self._completer = readline.get_completer() - self._delims = readline.get_completer_delims() + self._original_completer = readline.get_completer() + self._original_delims = readline.get_completer_delims() readline.set_completer(self.complete) readline.set_completer_delims(' \t\n;') @@ -57,5 +57,5 @@ class Completer(object): readline.parse_and_bind('tab: complete') def __exit__(self, unused_type, unused_value, unused_traceback): - readline.set_completer_delims(self._delims) - readline.set_completer(self._completer) + readline.set_completer_delims(self._original_delims) + readline.set_completer(self._original_completer) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 005e2ba9c..20c6be156 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -13,7 +13,7 @@ WIDTH = 72 HEIGHT = 20 DSELECT_HELP = ( - "Use the arrow keys or tab to move between window elements. Space can be " + "Use the arrow keys or Tab to move between window elements. Space can be " "used to complete the input path with the selected element in the " "directory window. Pressing enter will select the currently highlighted " "button.") @@ -559,7 +559,9 @@ class NoninteractiveDisplay(object): """Simulate prompting the user for a directory. This function returns default if it is not ``None``, otherwise, - an exception is raised. + an exception is raised explaining the problem. If cli_flag is + not ``None``, the error message will include the flag that can + be used to set this value with the CLI. :param str message: prompt to give the user :param default: default value to return (if one exists) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 188a4d9da..2fba11869 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -449,7 +449,9 @@ class IDisplay(zope.interface.Interface): :param str message: prompt to give the user :param default: the default value to return, if one exists, when using the NoninteractiveDisplay - :param str cli_flag: option used to set this value with the CLI + :param str cli_flag: option used to set this value with the CLI, + if one exists, to be included in error messages given by + NoninteractiveDisplay :returns: tuple of the form (`code`, `string`) where `code` - int display exit code From fe078dfb954abd89dab04a807ea9374744ecb84c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 25 Mar 2016 14:31:16 -0700 Subject: [PATCH 33/34] fixes #2712 --- Dockerfile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 71e217659..0e6700f90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,10 +33,11 @@ RUN /opt/letsencrypt/src/letsencrypt-auto-source/letsencrypt-auto --os-packages- # Dockerfile we make sure we cache as much as possible -COPY setup.py README.rst CHANGES.rst MANIFEST.in /opt/letsencrypt/src/ +COPY setup.py README.rst CHANGES.rst MANIFEST.in letsencrypt-auto-source/pieces/pipstrap.py /opt/letsencrypt/src/ -# all above files are necessary for setup.py, however, package source -# code directory has to be copied separately to a subdirectory... +# all above files are necessary for setup.py and venv setup, however, +# package source code directory has to be copied separately to a +# subdirectory... # https://docs.docker.com/reference/builder/#copy: "If is a # directory, the entire contents of the directory are copied, # including filesystem metadata. Note: The directory itself is not @@ -49,7 +50,11 @@ COPY letsencrypt-apache /opt/letsencrypt/src/letsencrypt-apache/ COPY letsencrypt-nginx /opt/letsencrypt/src/letsencrypt-nginx/ -RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ +RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv + +# PATH is set now so pipstrap upgrades the correct v(env) +ENV PATH /opt/letsencrypt/venv/bin:$PATH +RUN /opt/letsencrypt/venv/bin/python /opt/letsencrypt/src/pipstrap.py && \ /opt/letsencrypt/venv/bin/pip install \ -e /opt/letsencrypt/src/acme \ -e /opt/letsencrypt/src \ @@ -61,6 +66,4 @@ RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ # this might also help in debugging: you can "docker run --entrypoint # bash" and investigate, apply patches, etc. -ENV PATH /opt/letsencrypt/venv/bin:$PATH - ENTRYPOINT [ "letsencrypt" ] From c71b23754ce7fdfef65d90a8887a646f97d1f500 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 25 Mar 2016 14:47:12 -0700 Subject: [PATCH 34/34] What's a correct v? --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0e6700f90..ccbb07b95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,7 @@ COPY letsencrypt-nginx /opt/letsencrypt/src/letsencrypt-nginx/ RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv -# PATH is set now so pipstrap upgrades the correct v(env) +# PATH is set now so pipstrap upgrades the correct (v)env ENV PATH /opt/letsencrypt/venv/bin:$PATH RUN /opt/letsencrypt/venv/bin/python /opt/letsencrypt/src/pipstrap.py && \ /opt/letsencrypt/venv/bin/pip install \