mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
Merge remote-tracking branch 'letsencrypt/master'
This commit is contained in:
commit
6321719ba3
26 changed files with 912 additions and 489 deletions
15
Dockerfile
15
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 <src> 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" ]
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<VirtualHost 173.192.30.7:80 [2607:f0d0:1005:99::3:1337]:80>
|
||||
DocumentRoot /xxxx/
|
||||
ServerName noodles.net.nz
|
||||
ServerAlias www.noodles.net.nz
|
||||
CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
|
||||
<Directory "/xxxx/">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
|
||||
<VirtualHost 173.192.30.7:443 [2607:f0d0:1005:99::3:1337]:443>
|
||||
DocumentRoot /xxxx/
|
||||
ServerName noodles.net.nz
|
||||
ServerAlias www.noodles.net.nz
|
||||
CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
|
||||
<Directory "xxxx">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
|
||||
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 /xxxx/noodles.net.nz.crt
|
||||
SSLCertificateKeyFile /xxxx/noodles.net.nz.key
|
||||
|
||||
Header set Strict-Transport-Security "max-age=31536000; preload"
|
||||
</VirtualHost>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<VirtualHost *:80 [::]:80>
|
||||
DocumentRoot /tmp
|
||||
ServerName example.com
|
||||
ServerAlias www.example.com
|
||||
CustomLog ${APACHE_LOG_DIR}/example.log combined
|
||||
<Directory "/tmp">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<VirtualHost *:443 [::]:443>
|
||||
DocumentRoot /tmp
|
||||
ServerName example.com
|
||||
ServerAlias www.example.com
|
||||
CustomLog ${APACHE_LOG_DIR}/example.log combined
|
||||
<Directory "/tmp">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
|
||||
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
|
||||
</VirtualHost>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<VirtualHost [::]:80 *:80>
|
||||
DocumentRoot /tmp
|
||||
ServerName example.com
|
||||
ServerAlias www.example.com
|
||||
CustomLog ${APACHE_LOG_DIR}/example.log combined
|
||||
<Directory "/tmp">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<VirtualHost [::]:443 *:443>
|
||||
DocumentRoot /tmp
|
||||
ServerName example.com
|
||||
ServerAlias www.example.com
|
||||
CustomLog ${APACHE_LOG_DIR}/example.log combined
|
||||
<Directory "/tmp">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
|
||||
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
|
||||
</VirtualHost>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<VirtualHost *:80>
|
||||
<VirtualHost *:80 [::]:80>
|
||||
|
||||
ServerName ip-172-30-0-17
|
||||
ServerAdmin webmaster@localhost
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -21,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
|
||||
|
||||
import letsencrypt.plugins.selection as plugin_selection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -33,9 +32,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,147 +99,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 not now:
|
||||
# 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):
|
||||
"""
|
||||
Return True if a particular config variable has been set by the user
|
||||
|
|
@ -256,7 +115,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)
|
||||
|
|
@ -321,7 +180,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:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,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__)
|
||||
|
|
@ -534,7 +535,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?")
|
||||
|
||||
|
|
|
|||
61
letsencrypt/display/completer.py
Normal file
61
letsencrypt/display/completer.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""Provides Tab completion when prompting users for a path."""
|
||||
import glob
|
||||
# readline module is not available on all systems
|
||||
try:
|
||||
import readline
|
||||
except ImportError:
|
||||
import letsencrypt.display.dummy_readline as readline
|
||||
|
||||
|
||||
class Completer(object):
|
||||
"""Provides Tab completion when prompting users for a path.
|
||||
|
||||
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:
|
||||
|
||||
with Completer():
|
||||
raw_input()
|
||||
|
||||
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._iter = self._original_completer = self._original_delims = None
|
||||
|
||||
def complete(self, text, state):
|
||||
"""Provides path completion 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._original_completer = readline.get_completer()
|
||||
self._original_delims = readline.get_completer_delims()
|
||||
|
||||
readline.set_completer(self.complete)
|
||||
readline.set_completer_delims(' \t\n;')
|
||||
|
||||
# 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._original_delims)
|
||||
readline.set_completer(self._original_completer)
|
||||
21
letsencrypt/display/dummy_readline.py
Normal file
21
letsencrypt/display/dummy_readline.py
Normal file
|
|
@ -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."""
|
||||
|
|
@ -9,130 +9,10 @@ from letsencrypt import interfaces
|
|||
from letsencrypt import le_util
|
||||
from letsencrypt.display import util as display_util
|
||||
|
||||
|
||||
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 +62,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 +88,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 +136,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 +145,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 +180,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 +196,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 +215,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}"
|
||||
|
|
|
|||
|
|
@ -7,10 +7,18 @@ import zope.interface
|
|||
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import errors
|
||||
from letsencrypt.display import completer
|
||||
|
||||
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 +29,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 +45,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."""
|
||||
|
|
@ -118,7 +128,6 @@ class NcursesDisplay(object):
|
|||
|
||||
return code, int(index) - 1
|
||||
|
||||
|
||||
def input(self, message, **unused_kwargs):
|
||||
"""Display an input box to the user.
|
||||
|
||||
|
|
@ -132,11 +141,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.
|
||||
|
||||
|
|
@ -174,6 +182,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):
|
||||
|
|
@ -317,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.
|
||||
|
|
@ -373,7 +409,6 @@ class FileDisplay(object):
|
|||
|
||||
self.outfile.write(side_frame)
|
||||
|
||||
|
||||
def _get_valid_int_ans(self, max_):
|
||||
"""Get a numerical selection.
|
||||
|
||||
|
|
@ -409,6 +444,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"""
|
||||
|
|
@ -483,7 +519,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
|
||||
|
|
@ -520,6 +555,25 @@ 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 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)
|
||||
: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.
|
||||
|
|
|
|||
|
|
@ -443,6 +443,22 @@ 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,
|
||||
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
|
||||
`string` - input entered by the user
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class IValidator(zope.interface.Interface):
|
||||
"""Configuration validator."""
|
||||
|
|
|
|||
|
|
@ -32,6 +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 plug_sel
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -406,7 +407,7 @@ def install(config, plugins):
|
|||
# this function ...
|
||||
|
||||
try:
|
||||
installer, _ = cli.choose_configurator_plugins(config, plugins, "install")
|
||||
installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "install")
|
||||
except errors.PluginSelectionError as e:
|
||||
return e.message
|
||||
|
||||
|
|
@ -481,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 = cli.choose_configurator_plugins(config, plugins, "run")
|
||||
installer, authenticator = plug_sel.choose_configurator_plugins(config, plugins, "run")
|
||||
except errors.PluginSelectionError as e:
|
||||
return e.message
|
||||
|
||||
|
|
@ -511,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 = cli.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
|
||||
|
|
|
|||
|
|
@ -104,14 +104,24 @@ 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):
|
||||
"""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 + 2 and str_addr[endIndex + 1] == ':':
|
||||
port = str_addr[endIndex + 2:]
|
||||
return cls((host, port), ipv6=True)
|
||||
else:
|
||||
tup = str_addr.partition(':')
|
||||
return cls((tup[0], tup[2]))
|
||||
|
||||
def __str__(self):
|
||||
if self.tup[1]:
|
||||
|
|
@ -120,7 +130,16 @@ class Addr(object):
|
|||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.tup == other.tup
|
||||
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
|
||||
self.tup[1] == other.tup[1])
|
||||
else:
|
||||
return self.tup == other.tup
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
|
|
@ -136,7 +155,44 @@ 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(":")
|
||||
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]
|
||||
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 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
|
||||
|
||||
|
||||
class TLSSNI01(object):
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ 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")
|
||||
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")
|
||||
|
|
@ -89,22 +94,49 @@ 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")
|
||||
self.assertEqual(self.addr6.get_ipv6_exploded(),
|
||||
"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.addr8.get_ipv6_exploded(),
|
||||
"fe00:1:2:3:4:5:6:7")
|
||||
|
||||
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)
|
||||
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
|
||||
set_a = set([self.addr1, self.addr2])
|
||||
|
|
@ -114,6 +146,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."""
|
||||
|
|
|
|||
273
letsencrypt/plugins/selection.py
Normal file
273
letsencrypt/plugins/selection.py
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
"""Decide which plugins to use for authentication & installation"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
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__)
|
||||
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
|
||||
|
||||
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.installer 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 = pick_configurator(config, req_inst, plugins)
|
||||
else:
|
||||
if need_inst or req_inst:
|
||||
installer = pick_installer(config, req_inst, plugins)
|
||||
if need_auth:
|
||||
authenticator = 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 not now:
|
||||
# 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)
|
||||
149
letsencrypt/plugins/selection_test.py
Normal file
149
letsencrypt/plugins/selection_test.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -203,12 +203,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.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'])
|
||||
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):
|
||||
|
|
@ -499,7 +499,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)
|
||||
|
|
|
|||
|
|
@ -460,9 +460,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):
|
||||
|
|
|
|||
102
letsencrypt/tests/display/completer_test.py
Normal file
102
letsencrypt/tests/display/completer_test.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""Test letsencrypt.display.completer."""
|
||||
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):
|
||||
"""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_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)
|
||||
|
||||
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()
|
||||
|
||||
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_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_context_manager_with_mock_readline(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
|
||||
|
||||
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
|
||||
|
|
@ -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.display.ops 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.display.ops import pick_authenticator
|
||||
self._test(pick_authenticator, (interfaces.IAuthenticator,))
|
||||
|
||||
def test_installer(self):
|
||||
from letsencrypt.display.ops 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))
|
||||
|
||||
|
||||
class GetEmailTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.display.ops.get_email."""
|
||||
|
||||
|
|
@ -241,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)
|
||||
|
|
@ -339,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
|
||||
|
|
@ -355,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"])
|
||||
|
|
@ -371,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 = (
|
||||
|
|
@ -397,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
|
||||
|
|
@ -445,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"]
|
||||
|
|
@ -467,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"]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -227,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 = [
|
||||
|
|
@ -280,6 +294,7 @@ class FileOutputDisplayTest(unittest.TestCase):
|
|||
self.displayer._get_valid_int_ans(3),
|
||||
(display_util.CANCEL, -1))
|
||||
|
||||
|
||||
class NoninteractiveDisplayTest(unittest.TestCase):
|
||||
"""Test non-interactive display.
|
||||
|
||||
|
|
@ -320,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."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue