mirror of
https://github.com/certbot/certbot.git
synced 2026-07-28 08:45:22 +02:00
Merge remote-tracking branch 'origin/master' into must-staple
This commit is contained in:
+937
@@ -0,0 +1,937 @@
|
||||
"""Certbot command line argument & config processing."""
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import glob
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import configargparse
|
||||
import OpenSSL
|
||||
import six
|
||||
|
||||
import certbot
|
||||
|
||||
from certbot import constants
|
||||
from certbot import crypto_util
|
||||
from certbot import errors
|
||||
from certbot import hooks
|
||||
from certbot import interfaces
|
||||
from certbot import le_util
|
||||
|
||||
from certbot.plugins import disco as plugins_disco
|
||||
import certbot.plugins.selection as plugin_selection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global, to save us from a lot of argument passing within the scope of this module
|
||||
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/certbot/bin/certbot"
|
||||
# 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
|
||||
|
||||
LEAUTO = "letsencrypt-auto"
|
||||
if "CERTBOT_AUTO" in os.environ:
|
||||
# if we're here, this is probably going to be certbot-auto, unless the
|
||||
# user saved the script under a different name
|
||||
LEAUTO = os.path.basename(os.environ["CERTBOT_AUTO"])
|
||||
|
||||
fragment = os.path.join(".local", "share", "letsencrypt")
|
||||
cli_command = LEAUTO if fragment in sys.argv[0] else "certbot"
|
||||
|
||||
# Argparse's help formatting has a lot of unhelpful peculiarities, so we want
|
||||
# to replace as much of it as we can...
|
||||
|
||||
# This is the stub to include in help generated by argparse
|
||||
|
||||
SHORT_USAGE = """
|
||||
{0} [SUBCOMMAND] [options] [-d domain] [-d domain] ...
|
||||
|
||||
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
|
||||
it will attempt to use a webserver both for obtaining and installing the
|
||||
cert. Major SUBCOMMANDS are:
|
||||
|
||||
(default) run Obtain & install a cert in your current webserver
|
||||
certonly Obtain cert, but do not install it (aka "auth")
|
||||
install Install a previously obtained cert in a server
|
||||
renew Renew previously obtained certs that are near expiry
|
||||
revoke Revoke a previously obtained certificate
|
||||
rollback Rollback server configuration changes made during install
|
||||
config_changes Show changes made to server config during installation
|
||||
plugins Display information about installed plugins
|
||||
|
||||
""".format(cli_command)
|
||||
|
||||
# This is the short help for certbot --help, where we disable argparse
|
||||
# altogether
|
||||
USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert:
|
||||
|
||||
%s
|
||||
--standalone Run a standalone webserver for authentication
|
||||
%s
|
||||
--webroot Place files in a server's webroot folder for authentication
|
||||
|
||||
OR use different plugins to obtain (authenticate) the cert and then install it:
|
||||
|
||||
--authenticator standalone --installer apache
|
||||
|
||||
More detailed help:
|
||||
|
||||
-h, --help [topic] print this message, or detailed help on a topic;
|
||||
the available topics are:
|
||||
|
||||
all, automation, paths, security, testing, or any of the subcommands or
|
||||
plugins (certonly, install, nginx, apache, standalone, webroot, etc)
|
||||
"""
|
||||
|
||||
|
||||
# These argparse parameters should be removed when detecting defaults.
|
||||
ARGPARSE_PARAMS_TO_REMOVE = ("const", "nargs", "type",)
|
||||
|
||||
|
||||
# These sets are used when to help detect options set by the user.
|
||||
EXIT_ACTIONS = set(("help", "version",))
|
||||
|
||||
|
||||
ZERO_ARG_ACTIONS = set(("store_const", "store_true",
|
||||
"store_false", "append_const", "count",))
|
||||
|
||||
|
||||
# Maps a config option to a set of config options that may have modified it.
|
||||
# This dictionary is used recursively, so if A modifies B and B modifies C,
|
||||
# it is determined that C was modified by the user if A was modified.
|
||||
VAR_MODIFIERS = {"account": set(("server",)),
|
||||
"server": set(("dry_run", "staging",)),
|
||||
"webroot_map": set(("webroot_path",))}
|
||||
|
||||
|
||||
def report_config_interaction(modified, modifiers):
|
||||
"""Registers config option interaction to be checked by set_by_cli.
|
||||
|
||||
This function can be called by during the __init__ or
|
||||
add_parser_arguments methods of plugins to register interactions
|
||||
between config options.
|
||||
|
||||
:param modified: config options that can be modified by modifiers
|
||||
:type modified: iterable or str
|
||||
:param modifiers: config options that modify modified
|
||||
:type modifiers: iterable or str
|
||||
|
||||
"""
|
||||
if isinstance(modified, str):
|
||||
modified = (modified,)
|
||||
if isinstance(modifiers, str):
|
||||
modifiers = (modifiers,)
|
||||
|
||||
for var in modified:
|
||||
VAR_MODIFIERS.setdefault(var, set()).update(modifiers)
|
||||
|
||||
|
||||
def usage_strings(plugins):
|
||||
"""Make usage strings late so that plugins can be initialised late"""
|
||||
if "nginx" in plugins:
|
||||
nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
|
||||
else:
|
||||
nginx_doc = "(nginx support is experimental, buggy, and not installed by default)"
|
||||
if "apache" in plugins:
|
||||
apache_doc = "--apache Use the Apache plugin for authentication & installation"
|
||||
else:
|
||||
apache_doc = "(the apache plugin is not installed)"
|
||||
return USAGE % (apache_doc, nginx_doc), SHORT_USAGE
|
||||
|
||||
|
||||
def possible_deprecation_warning(config):
|
||||
"A deprecation warning for users with the old, not-self-upgrading letsencrypt-auto."
|
||||
if cli_command != LEAUTO:
|
||||
return
|
||||
if config.no_self_upgrade:
|
||||
# users setting --no-self-upgrade might be hanging on a clent version like 0.3.0
|
||||
# or 0.5.0 which is the new script, but doesn't set CERTBOT_AUTO; they don't
|
||||
# need warnings
|
||||
return
|
||||
if "CERTBOT_AUTO" not in os.environ:
|
||||
logger.warn("You are running with an old copy of letsencrypt-auto that does "
|
||||
"not receive updates, and is less reliable than more recent versions. "
|
||||
"We recommend upgrading to the latest certbot-auto script, or using native "
|
||||
"OS packages.")
|
||||
|
||||
|
||||
class _Default(object):
|
||||
"""A class to use as a default to detect if a value is set by a user"""
|
||||
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _Default)
|
||||
|
||||
def __hash__(self):
|
||||
return id(_Default)
|
||||
|
||||
def __nonzero__(self):
|
||||
return self.__bool__()
|
||||
|
||||
|
||||
def set_by_cli(var):
|
||||
"""
|
||||
Return True if a particular config variable has been set by the user
|
||||
(CLI or config file) including if the user explicitly set it to the
|
||||
default. Returns False if the variable was assigned a default value.
|
||||
"""
|
||||
detector = set_by_cli.detector
|
||||
if detector is None:
|
||||
# Setup on first run: `detector` is a weird version of config in which
|
||||
# the default value of every attribute is wrangled to be boolean-false
|
||||
plugins = plugins_disco.PluginsRegistry.find_all()
|
||||
# reconstructed_args == sys.argv[1:], or whatever was passed to main()
|
||||
reconstructed_args = helpful_parser.args + [helpful_parser.verb]
|
||||
detector = set_by_cli.detector = prepare_and_parse_args(
|
||||
plugins, reconstructed_args, detect_defaults=True)
|
||||
# propagate plugin requests: eg --standalone modifies config.authenticator
|
||||
detector.authenticator, detector.installer = (
|
||||
plugin_selection.cli_plugin_requests(detector))
|
||||
logger.debug("Default Detector is %r", detector)
|
||||
|
||||
if not isinstance(getattr(detector, var), _Default):
|
||||
return True
|
||||
|
||||
for modifier in VAR_MODIFIERS.get(var, []):
|
||||
if set_by_cli(modifier):
|
||||
return True
|
||||
|
||||
return False
|
||||
# static housekeeping var
|
||||
set_by_cli.detector = None
|
||||
|
||||
|
||||
def argparse_type(variable):
|
||||
"Return our argparse type function for a config variable (default: str)"
|
||||
# pylint: disable=protected-access
|
||||
for action in helpful_parser.parser._actions:
|
||||
if action.type is not None and action.dest == variable:
|
||||
return action.type
|
||||
return str
|
||||
|
||||
def read_file(filename, mode="rb"):
|
||||
"""Returns the given file's contents.
|
||||
|
||||
:param str filename: path to file
|
||||
:param str mode: open mode (see `open`)
|
||||
|
||||
:returns: absolute path of filename and its contents
|
||||
:rtype: tuple
|
||||
|
||||
:raises argparse.ArgumentTypeError: File does not exist or is not readable.
|
||||
|
||||
"""
|
||||
try:
|
||||
filename = os.path.abspath(filename)
|
||||
return filename, open(filename, mode).read()
|
||||
except IOError as exc:
|
||||
raise argparse.ArgumentTypeError(exc.strerror)
|
||||
|
||||
|
||||
def flag_default(name):
|
||||
"""Default value for CLI flag."""
|
||||
# XXX: this is an internal housekeeping notion of defaults before
|
||||
# argparse has been set up; it is not accurate for all flags. Call it
|
||||
# with caution. Plugin defaults are missing, and some things are using
|
||||
# defaults defined in this file, not in constants.py :(
|
||||
return constants.CLI_DEFAULTS[name]
|
||||
|
||||
|
||||
def config_help(name, hidden=False):
|
||||
"""Extract the help message for an `.IConfig` attribute."""
|
||||
if hidden:
|
||||
return argparse.SUPPRESS
|
||||
else:
|
||||
return interfaces.IConfig[name].__doc__
|
||||
|
||||
|
||||
class HelpfulArgumentGroup(object):
|
||||
"""Emulates an argparse group for use with HelpfulArgumentParser.
|
||||
|
||||
This class is used in the add_group method of HelpfulArgumentParser.
|
||||
Command line arguments can be added to the group, but help
|
||||
suppression and default detection is applied by
|
||||
HelpfulArgumentParser when necessary.
|
||||
|
||||
"""
|
||||
def __init__(self, helpful_arg_parser, topic):
|
||||
self._parser = helpful_arg_parser
|
||||
self._topic = topic
|
||||
|
||||
def add_argument(self, *args, **kwargs):
|
||||
"""Add a new command line argument to the argument group."""
|
||||
self._parser.add(self._topic, *args, **kwargs)
|
||||
|
||||
|
||||
class HelpfulArgumentParser(object):
|
||||
"""Argparse Wrapper.
|
||||
|
||||
This class wraps argparse, adding the ability to make --help less
|
||||
verbose, and request help on specific subcategories at a time, eg
|
||||
'certbot --help security' for security options.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, args, plugins, detect_defaults=False):
|
||||
from certbot import main
|
||||
self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert,
|
||||
"config_changes": main.config_changes, "run": main.run,
|
||||
"install": main.install, "plugins": main.plugins_cmd,
|
||||
"renew": main.renew, "revoke": main.revoke,
|
||||
"rollback": main.rollback, "everything": main.run}
|
||||
|
||||
# List of topics for which additional help can be provided
|
||||
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
|
||||
|
||||
plugin_names = list(plugins)
|
||||
self.help_topics = HELP_TOPICS + plugin_names + [None]
|
||||
usage, short_usage = usage_strings(plugins)
|
||||
self.parser = configargparse.ArgParser(
|
||||
usage=short_usage,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
args_for_setting_config_path=["-c", "--config"],
|
||||
default_config_files=flag_default("config_files"))
|
||||
|
||||
# This is the only way to turn off overly verbose config flag documentation
|
||||
self.parser._add_config_file_help = False # pylint: disable=protected-access
|
||||
|
||||
self.detect_defaults = detect_defaults
|
||||
|
||||
self.args = args
|
||||
self.determine_verb()
|
||||
help1 = self.prescan_for_flag("-h", self.help_topics)
|
||||
help2 = self.prescan_for_flag("--help", self.help_topics)
|
||||
assert max(True, "a") == "a", "Gravity changed direction"
|
||||
self.help_arg = max(help1, help2)
|
||||
if self.help_arg is True:
|
||||
# just --help with no topic; avoid argparse altogether
|
||||
print(usage)
|
||||
sys.exit(0)
|
||||
self.visible_topics = self.determine_help_topics(self.help_arg)
|
||||
self.groups = {} # elements are added by .add_group()
|
||||
|
||||
def parse_args(self):
|
||||
"""Parses command line arguments and returns the result.
|
||||
|
||||
:returns: parsed command line arguments
|
||||
:rtype: argparse.Namespace
|
||||
|
||||
"""
|
||||
parsed_args = self.parser.parse_args(self.args)
|
||||
parsed_args.func = self.VERBS[self.verb]
|
||||
parsed_args.verb = self.verb
|
||||
|
||||
if self.detect_defaults:
|
||||
return parsed_args
|
||||
|
||||
# Do any post-parsing homework here
|
||||
|
||||
if parsed_args.staging or parsed_args.dry_run:
|
||||
if parsed_args.server not in (flag_default("server"), constants.STAGING_URI):
|
||||
conflicts = ["--staging"] if parsed_args.staging else []
|
||||
conflicts += ["--dry-run"] if parsed_args.dry_run else []
|
||||
raise errors.Error("--server value conflicts with {0}".format(
|
||||
" and ".join(conflicts)))
|
||||
|
||||
parsed_args.server = constants.STAGING_URI
|
||||
|
||||
if parsed_args.dry_run:
|
||||
if self.verb not in ["certonly", "renew"]:
|
||||
raise errors.Error("--dry-run currently only works with the "
|
||||
"'certonly' or 'renew' subcommands (%r)" % self.verb)
|
||||
parsed_args.break_my_certs = parsed_args.staging = True
|
||||
if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")):
|
||||
# The user has a prod account, but might not have a staging
|
||||
# one; we don't want to start trying to perform interactive registration
|
||||
parsed_args.tos = True
|
||||
parsed_args.register_unsafely_without_email = True
|
||||
|
||||
if parsed_args.csr:
|
||||
if parsed_args.allow_subset_of_names:
|
||||
raise errors.Error("--allow-subset-of-names "
|
||||
"cannot be used with --csr")
|
||||
self.handle_csr(parsed_args)
|
||||
|
||||
hooks.validate_hooks(parsed_args)
|
||||
|
||||
return parsed_args
|
||||
|
||||
def handle_csr(self, parsed_args):
|
||||
"""Process a --csr flag."""
|
||||
if parsed_args.verb != "certonly":
|
||||
raise errors.Error("Currently, a CSR file may only be specified "
|
||||
"when obtaining a new or replacement "
|
||||
"via the certonly command. Please try the "
|
||||
"certonly command instead.")
|
||||
|
||||
try:
|
||||
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der")
|
||||
typ = OpenSSL.crypto.FILETYPE_ASN1
|
||||
domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
except OpenSSL.crypto.Error:
|
||||
try:
|
||||
e1 = traceback.format_exc()
|
||||
typ = OpenSSL.crypto.FILETYPE_PEM
|
||||
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem")
|
||||
domains = crypto_util.get_sans_from_csr(csr.data, typ)
|
||||
except OpenSSL.crypto.Error:
|
||||
logger.debug("DER CSR parse error %s", e1)
|
||||
logger.debug("PEM CSR parse error %s", traceback.format_exc())
|
||||
raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0]))
|
||||
|
||||
# This is not necessary for webroot to work, however,
|
||||
# obtain_certificate_from_csr requires parsed_args.domains to be set
|
||||
for domain in domains:
|
||||
add_domains(parsed_args, domain)
|
||||
|
||||
if not domains:
|
||||
# TODO: add CN to domains instead:
|
||||
raise errors.Error(
|
||||
"Unfortunately, your CSR %s needs to have a SubjectAltName for every domain"
|
||||
% parsed_args.csr[0])
|
||||
|
||||
parsed_args.actual_csr = (csr, typ)
|
||||
csr_domains, config_domains = set(domains), set(parsed_args.domains)
|
||||
if csr_domains != config_domains:
|
||||
raise errors.ConfigurationError(
|
||||
"Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}"
|
||||
.format(", ".join(csr_domains), ", ".join(config_domains)))
|
||||
|
||||
|
||||
def determine_verb(self):
|
||||
"""Determines the verb/subcommand provided by the user.
|
||||
|
||||
This function works around some of the limitations of argparse.
|
||||
|
||||
"""
|
||||
if "-h" in self.args or "--help" in self.args:
|
||||
# all verbs double as help arguments; don't get them confused
|
||||
self.verb = "help"
|
||||
return
|
||||
|
||||
for i, token in enumerate(self.args):
|
||||
if token in self.VERBS:
|
||||
verb = token
|
||||
if verb == "auth":
|
||||
verb = "certonly"
|
||||
if verb == "everything":
|
||||
verb = "run"
|
||||
self.verb = verb
|
||||
self.args.pop(i)
|
||||
return
|
||||
|
||||
self.verb = "run"
|
||||
|
||||
def prescan_for_flag(self, flag, possible_arguments):
|
||||
"""Checks cli input for flags.
|
||||
|
||||
Check for a flag, which accepts a fixed set of possible arguments, in
|
||||
the command line; we will use this information to configure argparse's
|
||||
help correctly. Return the flag's argument, if it has one that matches
|
||||
the sequence @possible_arguments; otherwise return whether the flag is
|
||||
present.
|
||||
|
||||
"""
|
||||
if flag not in self.args:
|
||||
return False
|
||||
pos = self.args.index(flag)
|
||||
try:
|
||||
nxt = self.args[pos + 1]
|
||||
if nxt in possible_arguments:
|
||||
return nxt
|
||||
except IndexError:
|
||||
pass
|
||||
return True
|
||||
|
||||
def add(self, topic, *args, **kwargs):
|
||||
"""Add a new command line argument.
|
||||
|
||||
:param str: help topic this should be listed under, can be None for
|
||||
"always documented"
|
||||
:param list *args: the names of this argument flag
|
||||
:param dict **kwargs: various argparse settings for this argument
|
||||
|
||||
"""
|
||||
|
||||
if self.detect_defaults:
|
||||
kwargs = self.modify_kwargs_for_default_detection(**kwargs)
|
||||
|
||||
if self.visible_topics[topic]:
|
||||
if topic in self.groups:
|
||||
group = self.groups[topic]
|
||||
group.add_argument(*args, **kwargs)
|
||||
else:
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
else:
|
||||
kwargs["help"] = argparse.SUPPRESS
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
|
||||
def modify_kwargs_for_default_detection(self, **kwargs):
|
||||
"""Modify an arg so we can check if it was set by the user.
|
||||
|
||||
Changes the parameters given to argparse when adding an argument
|
||||
so we can properly detect if the value was set by the user.
|
||||
|
||||
:param dict kwargs: various argparse settings for this argument
|
||||
|
||||
:returns: a modified versions of kwargs
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
action = kwargs.get("action", None)
|
||||
if action not in EXIT_ACTIONS:
|
||||
kwargs["action"] = ("store_true" if action in ZERO_ARG_ACTIONS else
|
||||
"store")
|
||||
kwargs["default"] = _Default()
|
||||
for param in ARGPARSE_PARAMS_TO_REMOVE:
|
||||
kwargs.pop(param, None)
|
||||
|
||||
return kwargs
|
||||
|
||||
def add_deprecated_argument(self, argument_name, num_args):
|
||||
"""Adds a deprecated argument with the name argument_name.
|
||||
|
||||
Deprecated arguments are not shown in the help. If they are used
|
||||
on the command line, a warning is shown stating that the
|
||||
argument is deprecated and no other action is taken.
|
||||
|
||||
:param str argument_name: Name of deprecated argument.
|
||||
:param int nargs: Number of arguments the option takes.
|
||||
|
||||
"""
|
||||
le_util.add_deprecated_argument(
|
||||
self.parser.add_argument, argument_name, num_args)
|
||||
|
||||
def add_group(self, topic, **kwargs):
|
||||
"""Create a new argument group.
|
||||
|
||||
This method must be called once for every topic, however, calls
|
||||
to this function are left next to the argument definitions for
|
||||
clarity.
|
||||
|
||||
:param str topic: Name of the new argument group.
|
||||
|
||||
:returns: The new argument group.
|
||||
:rtype: `HelpfulArgumentGroup`
|
||||
|
||||
"""
|
||||
if self.visible_topics[topic]:
|
||||
self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)
|
||||
|
||||
return HelpfulArgumentGroup(self, topic)
|
||||
|
||||
def add_plugin_args(self, plugins):
|
||||
"""
|
||||
|
||||
Let each of the plugins add its own command line arguments, which
|
||||
may or may not be displayed as help topics.
|
||||
|
||||
"""
|
||||
for name, plugin_ep in six.iteritems(plugins):
|
||||
parser_or_group = self.add_group(name, description=plugin_ep.description)
|
||||
plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name)
|
||||
|
||||
def determine_help_topics(self, chosen_topic):
|
||||
"""
|
||||
|
||||
The user may have requested help on a topic, return a dict of which
|
||||
topics to display. @chosen_topic has prescan_for_flag's return type
|
||||
|
||||
:returns: dict
|
||||
|
||||
"""
|
||||
# topics maps each topic to whether it should be documented by
|
||||
# argparse on the command line
|
||||
if chosen_topic == "auth":
|
||||
chosen_topic = "certonly"
|
||||
if chosen_topic == "everything":
|
||||
chosen_topic = "run"
|
||||
if chosen_topic == "all":
|
||||
return dict([(t, True) for t in self.help_topics])
|
||||
elif not chosen_topic:
|
||||
return dict([(t, False) for t in self.help_topics])
|
||||
else:
|
||||
return dict([(t, t == chosen_topic) for t in self.help_topics])
|
||||
|
||||
|
||||
def prepare_and_parse_args(plugins, args, detect_defaults=False):
|
||||
"""Returns parsed command line arguments.
|
||||
|
||||
:param .PluginsRegistry plugins: available plugins
|
||||
:param list args: command line arguments with the program name removed
|
||||
|
||||
:returns: parsed command line arguments
|
||||
:rtype: argparse.Namespace
|
||||
|
||||
"""
|
||||
helpful = HelpfulArgumentParser(args, plugins, detect_defaults)
|
||||
|
||||
# --help is automatically provided by argparse
|
||||
helpful.add(
|
||||
None, "-v", "--verbose", dest="verbose_count", action="count",
|
||||
default=flag_default("verbose_count"), help="This flag can be used "
|
||||
"multiple times to incrementally increase the verbosity of output, "
|
||||
"e.g. -vvv.")
|
||||
helpful.add(
|
||||
None, "-t", "--text", dest="text_mode", action="store_true",
|
||||
help="Use the text output instead of the curses UI.")
|
||||
helpful.add(
|
||||
None, "-n", "--non-interactive", "--noninteractive",
|
||||
dest="noninteractive_mode", action="store_true",
|
||||
help="Run without ever asking for user input. This may require "
|
||||
"additional command line flags; the client will try to explain "
|
||||
"which ones are required if it finds one missing")
|
||||
helpful.add(
|
||||
None, "--dry-run", action="store_true", dest="dry_run",
|
||||
help="Perform a test run of the client, obtaining test (invalid) certs"
|
||||
" but not saving them to disk. This can currently only be used"
|
||||
" with the 'certonly' and 'renew' subcommands. \nNote: Although --dry-run"
|
||||
" tries to avoid making any persistent changes on a system, it "
|
||||
" is not completely side-effect free: if used with webserver authenticator plugins"
|
||||
" like apache and nginx, it makes and then reverts temporary config changes"
|
||||
" in order to obtain test certs, and reloads webservers to deploy and then"
|
||||
" roll back those changes. It also calls --pre-hook and --post-hook commands"
|
||||
" if they are defined because they may be necessary to accurately simulate"
|
||||
" renewal. --renew-hook commands are not called.")
|
||||
helpful.add(
|
||||
None, "--register-unsafely-without-email", action="store_true",
|
||||
help="Specifying this flag enables registering an account with no "
|
||||
"email address. This is strongly discouraged, because in the "
|
||||
"event of key loss or account compromise you will irrevocably "
|
||||
"lose access to your account. You will also be unable to receive "
|
||||
"notice about impending expiration or revocation of your "
|
||||
"certificates. Updates to the Subscriber Agreement will still "
|
||||
"affect you, and will be effective 14 days after posting an "
|
||||
"update to the web site.")
|
||||
helpful.add(None, "-m", "--email", help=config_help("email"))
|
||||
# positional arg shadows --domains, instead of appending, and
|
||||
# --domains is useful, because it can be stored in config
|
||||
#for subparser in parser_run, parser_auth, parser_install:
|
||||
# subparser.add_argument("domains", nargs="*", metavar="domain")
|
||||
helpful.add(None, "-d", "--domains", "--domain", dest="domains",
|
||||
metavar="DOMAIN", action=_DomainsAction, default=[],
|
||||
help="Domain names to apply. For multiple domains you can use "
|
||||
"multiple -d flags or enter a comma separated list of domains "
|
||||
"as a parameter.")
|
||||
helpful.add_group(
|
||||
"automation",
|
||||
description="Arguments for automating execution & other tweaks")
|
||||
helpful.add(
|
||||
"automation", "--keep-until-expiring", "--keep", "--reinstall",
|
||||
dest="reinstall", action="store_true",
|
||||
help="If the requested cert matches an existing cert, always keep the "
|
||||
"existing one until it is due for renewal (for the "
|
||||
"'run' subcommand this means reinstall the existing cert)")
|
||||
helpful.add(
|
||||
"automation", "--expand", action="store_true",
|
||||
help="If an existing cert covers some subset of the requested names, "
|
||||
"always expand and replace it with the additional names.")
|
||||
helpful.add(
|
||||
"automation", "--version", action="version",
|
||||
version="%(prog)s {0}".format(certbot.__version__),
|
||||
help="show program's version number and exit")
|
||||
helpful.add(
|
||||
"automation", "--force-renewal", "--renew-by-default",
|
||||
action="store_true", dest="renew_by_default", help="If a certificate "
|
||||
"already exists for the requested domains, renew it now, "
|
||||
"regardless of whether it is near expiry. (Often "
|
||||
"--keep-until-expiring is more appropriate). Also implies "
|
||||
"--expand.")
|
||||
helpful.add(
|
||||
"automation", "--allow-subset-of-names", action="store_true",
|
||||
help="When performing domain validation, do not consider it a failure "
|
||||
"if authorizations can not be obtained for a strict subset of "
|
||||
"the requested domains. This may be useful for allowing renewals for "
|
||||
"multiple domains to succeed even if some domains no longer point "
|
||||
"at this system. This option cannot be used with --csr.")
|
||||
helpful.add(
|
||||
"automation", "--agree-tos", dest="tos", action="store_true",
|
||||
help="Agree to the ACME Subscriber Agreement")
|
||||
helpful.add(
|
||||
"automation", "--account", metavar="ACCOUNT_ID",
|
||||
help="Account ID to use")
|
||||
helpful.add(
|
||||
"automation", "--duplicate", dest="duplicate", action="store_true",
|
||||
help="Allow making a certificate lineage that duplicates an existing one "
|
||||
"(both can be renewed in parallel)")
|
||||
helpful.add(
|
||||
"automation", "--os-packages-only", action="store_true",
|
||||
help="(letsencrypt-auto only) install OS package dependencies and then stop")
|
||||
helpful.add(
|
||||
"automation", "--no-self-upgrade", action="store_true",
|
||||
help="(letsencrypt-auto only) prevent the letsencrypt-auto script from"
|
||||
" upgrading itself to newer released versions")
|
||||
helpful.add(
|
||||
"automation", "-q", "--quiet", dest="quiet", action="store_true",
|
||||
help="Silence all output except errors. Useful for automation via cron."
|
||||
" Implies --non-interactive.")
|
||||
|
||||
helpful.add_group(
|
||||
"testing", description="The following flags are meant for "
|
||||
"testing purposes only! Do NOT change them, unless you "
|
||||
"really know what you're doing!")
|
||||
helpful.add(
|
||||
"testing", "--debug", action="store_true",
|
||||
help="Show tracebacks in case of errors, and allow letsencrypt-auto "
|
||||
"execution on experimental platforms")
|
||||
helpful.add(
|
||||
"testing", "--no-verify-ssl", action="store_true",
|
||||
help=config_help("no_verify_ssl"),
|
||||
default=flag_default("no_verify_ssl"))
|
||||
helpful.add(
|
||||
"testing", "--tls-sni-01-port", type=int,
|
||||
default=flag_default("tls_sni_01_port"),
|
||||
help=config_help("tls_sni_01_port"))
|
||||
helpful.add(
|
||||
"testing", "--http-01-port", type=int, dest="http01_port",
|
||||
default=flag_default("http01_port"), help=config_help("http01_port"))
|
||||
helpful.add(
|
||||
"testing", "--break-my-certs", action="store_true",
|
||||
help="Be willing to replace or renew valid certs with invalid "
|
||||
"(testing/staging) certs")
|
||||
helpful.add_group(
|
||||
"security", description="Security parameters & server settings")
|
||||
helpful.add(
|
||||
"security", "--rsa-key-size", type=int, metavar="N",
|
||||
default=flag_default("rsa_key_size"), help=config_help("rsa_key_size"))
|
||||
helpful.add(
|
||||
"security", "--must-staple", action="store_true",
|
||||
help=config_help("must_staple"), dest="must_staple", default=False)
|
||||
helpful.add(
|
||||
"security", "--redirect", action="store_true",
|
||||
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
|
||||
"authenticated vhost.", dest="redirect", default=None)
|
||||
helpful.add(
|
||||
"security", "--no-redirect", action="store_false",
|
||||
help="Do not automatically redirect all HTTP traffic to HTTPS for the newly "
|
||||
"authenticated vhost.", dest="redirect", default=None)
|
||||
helpful.add(
|
||||
"security", "--hsts", action="store_true",
|
||||
help="Add the Strict-Transport-Security header to every HTTP response."
|
||||
" Forcing browser to use always use SSL for the domain."
|
||||
" Defends against SSL Stripping.", dest="hsts", default=False)
|
||||
helpful.add(
|
||||
"security", "--no-hsts", action="store_false",
|
||||
help="Do not automatically add the Strict-Transport-Security header"
|
||||
" to every HTTP response.", dest="hsts", default=False)
|
||||
helpful.add(
|
||||
"security", "--uir", action="store_true",
|
||||
help="Add the \"Content-Security-Policy: upgrade-insecure-requests\""
|
||||
" header to every HTTP response. Forcing the browser to use"
|
||||
" https:// for every http:// resource.", dest="uir", default=None)
|
||||
helpful.add(
|
||||
"security", "--no-uir", action="store_false",
|
||||
help=" Do not automatically set the \"Content-Security-Policy:"
|
||||
" upgrade-insecure-requests\" header to every HTTP response.",
|
||||
dest="uir", default=None)
|
||||
helpful.add(
|
||||
"security", "--strict-permissions", action="store_true",
|
||||
help="Require that all configuration files are owned by the current "
|
||||
"user; only needed if your config is somewhere unsafe like /tmp/")
|
||||
|
||||
helpful.add_group(
|
||||
"renew", description="The 'renew' subcommand will attempt to renew all"
|
||||
" certificates (or more precisely, certificate lineages) you have"
|
||||
" previously obtained if they are close to expiry, and print a"
|
||||
" summary of the results. By default, 'renew' will reuse the options"
|
||||
" used to create obtain or most recently successfully renew each"
|
||||
" certificate lineage. You can try it with `--dry-run` first. For"
|
||||
" more fine-grained control, you can renew individual lineages with"
|
||||
" the `certonly` subcommand. Hooks are available to run commands "
|
||||
" before and after renewal; see XXX for more information on these.")
|
||||
|
||||
helpful.add(
|
||||
"renew", "--pre-hook",
|
||||
help="Command to be run in a shell before obtaining any certificates. Intended"
|
||||
" primarily for renewal, where it can be used to temporarily shut down a"
|
||||
" webserver that might conflict with the standalone plugin. This will "
|
||||
" only be called if a certificate is actually to be obtained/renewed. ")
|
||||
helpful.add(
|
||||
"renew", "--post-hook",
|
||||
help="Command to be run in a shell after attempting to obtain/renew "
|
||||
" certificates. Can be used to deploy renewed certificates, or to restart"
|
||||
" any servers that were stopped by --pre-hook.")
|
||||
helpful.add(
|
||||
"renew", "--renew-hook",
|
||||
help="Command to be run in a shell once for each successfully renewed certificate."
|
||||
"For this command, the shell variable $RENEWED_LINEAGE will point to the"
|
||||
"config live subdirectory containing the new certs and keys; the shell variable "
|
||||
"$RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains")
|
||||
|
||||
helpful.add_deprecated_argument("--agree-dev-preview", 0)
|
||||
|
||||
_create_subparsers(helpful)
|
||||
_paths_parser(helpful)
|
||||
# _plugins_parsing should be the last thing to act upon the main
|
||||
# parser (--help should display plugin-specific options last)
|
||||
_plugins_parsing(helpful, plugins)
|
||||
|
||||
if not detect_defaults:
|
||||
global helpful_parser # pylint: disable=global-statement
|
||||
helpful_parser = helpful
|
||||
return helpful.parse_args()
|
||||
|
||||
|
||||
def _create_subparsers(helpful):
|
||||
helpful.add_group("certonly", description="Options for modifying how a cert is obtained")
|
||||
helpful.add_group("install", description="Options for modifying how a cert is deployed")
|
||||
helpful.add_group("revoke", description="Options for revocation of certs")
|
||||
helpful.add_group("rollback", description="Options for reverting config changes")
|
||||
helpful.add_group("plugins", description="Plugin options")
|
||||
helpful.add_group("config_changes",
|
||||
description="Options for showing a history of config changes")
|
||||
helpful.add("config_changes", "--num", type=int,
|
||||
help="How many past revisions you want to be displayed")
|
||||
helpful.add(
|
||||
None, "--user-agent", default=None,
|
||||
help="Set a custom user agent string for the client. User agent strings allow "
|
||||
"the CA to collect high level statistics about success rates by OS and "
|
||||
"plugin. If you wish to hide your server OS version from the Let's "
|
||||
'Encrypt server, set this to "".')
|
||||
helpful.add("certonly",
|
||||
"--csr", type=read_file,
|
||||
help="Path to a Certificate Signing Request (CSR) in DER"
|
||||
" format; note that the .csr file *must* contain a Subject"
|
||||
" Alternative Name field for each domain you want certified."
|
||||
" Currently --csr only works with the 'certonly' subcommand'")
|
||||
helpful.add("rollback",
|
||||
"--checkpoints", type=int, metavar="N",
|
||||
default=flag_default("rollback_checkpoints"),
|
||||
help="Revert configuration N number of checkpoints.")
|
||||
helpful.add("plugins",
|
||||
"--init", action="store_true", help="Initialize plugins.")
|
||||
helpful.add("plugins",
|
||||
"--prepare", action="store_true", help="Initialize and prepare plugins.")
|
||||
helpful.add("plugins",
|
||||
"--authenticators", action="append_const", dest="ifaces",
|
||||
const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.")
|
||||
helpful.add("plugins",
|
||||
"--installers", action="append_const", dest="ifaces",
|
||||
const=interfaces.IInstaller, help="Limit to installer plugins only.")
|
||||
|
||||
|
||||
def _paths_parser(helpful):
|
||||
add = helpful.add
|
||||
verb = helpful.verb
|
||||
if verb == "help":
|
||||
verb = helpful.help_arg
|
||||
helpful.add_group(
|
||||
"paths", description="Arguments changing execution paths & servers")
|
||||
|
||||
cph = "Path to where cert is saved (with auth --csr), installed from or revoked."
|
||||
section = "paths"
|
||||
if verb in ("install", "revoke", "certonly"):
|
||||
section = verb
|
||||
if verb == "certonly":
|
||||
add(section, "--cert-path", type=os.path.abspath,
|
||||
default=flag_default("auth_cert_path"), help=cph)
|
||||
elif verb == "revoke":
|
||||
add(section, "--cert-path", type=read_file, required=True, help=cph)
|
||||
else:
|
||||
add(section, "--cert-path", type=os.path.abspath,
|
||||
help=cph, required=(verb == "install"))
|
||||
|
||||
section = "paths"
|
||||
if verb in ("install", "revoke"):
|
||||
section = verb
|
||||
# revoke --key-path reads a file, install --key-path takes a string
|
||||
add(section, "--key-path", required=(verb == "install"),
|
||||
type=((verb == "revoke" and read_file) or os.path.abspath),
|
||||
help="Path to private key for cert installation "
|
||||
"or revocation (if account key is missing)")
|
||||
|
||||
default_cp = None
|
||||
if verb == "certonly":
|
||||
default_cp = flag_default("auth_chain_path")
|
||||
add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath,
|
||||
help="Accompanying path to a full certificate chain (cert plus chain).")
|
||||
add("paths", "--chain-path", default=default_cp, type=os.path.abspath,
|
||||
help="Accompanying path to a certificate chain.")
|
||||
add("paths", "--config-dir", default=flag_default("config_dir"),
|
||||
help=config_help("config_dir"))
|
||||
add("paths", "--work-dir", default=flag_default("work_dir"),
|
||||
help=config_help("work_dir"))
|
||||
add("paths", "--logs-dir", default=flag_default("logs_dir"),
|
||||
help="Logs directory.")
|
||||
add("paths", "--server", default=flag_default("server"),
|
||||
help=config_help("server"))
|
||||
# overwrites server, handled in HelpfulArgumentParser.parse_args()
|
||||
add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
|
||||
help='Use the staging server to obtain test (invalid) certs; equivalent'
|
||||
' to --server ' + constants.STAGING_URI)
|
||||
|
||||
|
||||
def _plugins_parsing(helpful, plugins):
|
||||
helpful.add_group(
|
||||
"plugins", description="Certbot client supports an "
|
||||
"extensible plugins architecture. See '%(prog)s plugins' for a "
|
||||
"list of all installed plugins and their names. You can force "
|
||||
"a particular plugin by setting options provided below. Running "
|
||||
"--help <plugin_name> will list flags specific to that plugin.")
|
||||
helpful.add(
|
||||
"plugins", "-a", "--authenticator", help="Authenticator plugin name.")
|
||||
helpful.add(
|
||||
"plugins", "-i", "--installer", help="Installer plugin name (also used to find domains).")
|
||||
helpful.add(
|
||||
"plugins", "--configurator", help="Name of the plugin that is "
|
||||
"both an authenticator and an installer. Should not be used "
|
||||
"together with --authenticator or --installer.")
|
||||
helpful.add("plugins", "--apache", action="store_true",
|
||||
help="Obtain and install certs using Apache")
|
||||
helpful.add("plugins", "--nginx", action="store_true",
|
||||
help="Obtain and install certs using Nginx")
|
||||
helpful.add("plugins", "--standalone", action="store_true",
|
||||
help='Obtain certs using a "standalone" webserver.')
|
||||
helpful.add("plugins", "--manual", action="store_true",
|
||||
help='Provide laborious manual instructions for obtaining a cert')
|
||||
helpful.add("plugins", "--webroot", action="store_true",
|
||||
help='Obtain certs by placing files in a webroot directory.')
|
||||
|
||||
# things should not be reorder past/pre this comment:
|
||||
# plugins_group should be displayed in --help before plugin
|
||||
# specific groups (so that plugins_group.description makes sense)
|
||||
|
||||
helpful.add_plugin_args(plugins)
|
||||
|
||||
|
||||
class _DomainsAction(argparse.Action):
|
||||
"""Action class for parsing domains."""
|
||||
|
||||
def __call__(self, parser, namespace, domain, option_string=None):
|
||||
"""Just wrap add_domains in argparseese."""
|
||||
add_domains(namespace, domain)
|
||||
|
||||
|
||||
def add_domains(args_or_config, domains):
|
||||
"""Registers new domains to be used during the current client run.
|
||||
|
||||
Domains are not added to the list of requested domains if they have
|
||||
already been registered.
|
||||
|
||||
:param args_or_config: parsed command line arguments
|
||||
:type args_or_config: argparse.Namespace or
|
||||
configuration.NamespaceConfig
|
||||
:param str domain: one or more comma separated domains
|
||||
|
||||
:returns: domains after they have been normalized and validated
|
||||
:rtype: `list` of `str`
|
||||
|
||||
"""
|
||||
validated_domains = []
|
||||
for domain in domains.split(","):
|
||||
domain = le_util.enforce_domain_sanity(domain.strip())
|
||||
validated_domains.append(domain)
|
||||
if domain not in args_or_config.domains:
|
||||
args_or_config.domains.append(domain)
|
||||
|
||||
return validated_domains
|
||||
Reference in New Issue
Block a user