mirror of
https://github.com/certbot/certbot.git
synced 2026-07-26 07:39:52 +02:00
* Start of combined manual/script plugin * Return str from hooks.execute, not bytes * finish manual/script rewrite * delete old manual and script plugins * manually specify we want chall.token * use consistent quotes * specify chall for uri * s/script/hook * fix spacing on instructions * remove unneeded response argument * make achall more helpful * simplify perform * remove old test files * add start of manual_tests * fix ParseTest.test_help * stop using manual_test_mode in cli tests * Revert "make achall more helpful" This reverts commit54b01cea30. * use bad response/validation methods on achalls * simplify perform and cleanup environment * finish manual tests * Add HTTP manual hook integration test * add manual http scripts * Add manual DNS script integration test * remove references to the script plugin * they're hooks, not scripts * add --manual-public-ip-logging-ok to integration tests * use --pref-chall for dns integration * does dns work? * validate hooks * test hook validation * Revert "does dns work?" This reverts commit1224cc2961. * busy wait in manual-http-auth * remove DNS script test for now * Fix challenge prefix and add trailing . * Add comment about universal_newlines * Fix typo from0464ba2c4* fix nits and typos * Generalize HookCOmmandNotFound error * Add verify_exe_exists * Don't duplicate code in hooks.py * Revert changes to hooks.py * Use consistent hook error messages
273 lines
10 KiB
Python
273 lines
10 KiB
Python
"""Decide which plugins to use for authentication & installation"""
|
|
from __future__ import print_function
|
|
|
|
import os
|
|
import logging
|
|
|
|
import six
|
|
import zope.component
|
|
|
|
from certbot import errors
|
|
from certbot import interfaces
|
|
|
|
from certbot.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 ACME CA?"):
|
|
"""Pick authentication plugin."""
|
|
return pick_plugin(
|
|
config, default, plugins, question, (interfaces.IAuthenticator,))
|
|
|
|
|
|
def pick_plugin(config, default, plugins, question, ifaces):
|
|
"""Pick plugin.
|
|
|
|
:param certbot.interfaces.IConfig: Configuration
|
|
:param str default: Plugin name supplied by user or ``None``.
|
|
:param certbot.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(list(six.itervalues(prepared)), question)
|
|
if plugin_ep is None:
|
|
return None
|
|
else:
|
|
return plugin_ep.init()
|
|
elif len(prepared) == 1:
|
|
plugin_ep = list(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", force_interactive=True)
|
|
|
|
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()), 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,
|
|
force_interactive=True)
|
|
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 certbot.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.warning("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":
|
|
from certbot.cli import cli_command
|
|
msg = ('Certbot doesn\'t know how to automatically configure the web '
|
|
'server on this system. However, it can still get a certificate for '
|
|
'you. Please run "{0} certonly" to do so. You\'ll need to '
|
|
'manually configure your web server to use the resulting '
|
|
'certificate.').format(cli_command)
|
|
else:
|
|
msg = "{0} could not be determined or is not installed".format(cfg_type)
|
|
raise errors.PluginSelectionError(msg)
|