Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/certbot/certbot.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaklyon <46962656+Raklyon@users.noreply.github.com>2020-02-21 23:30:58 +0300
committerGitHub <noreply@github.com>2020-02-21 23:30:58 +0300
commit84b57fac9341453b12135cdf26d9ede092e2c3aa (patch)
tree3d4afe37ec41bdc348fe19abb3c909d79e886b94
parent7d79c91e9b52ef75d7ac86232dbbedc84f066587 (diff)
Refactor cli.py, splitting in it smaller submodules (#6803)
* Refactor cli.py into a package with submodules * Added unit tests for helpful module in cli. * Fixed linter errors * Fixed pylint issues * Updated changelog.md * Fixed test failing and mypy error. Appeared a new pylint error (seems to be in conflict with mypy) mypy require zope.interface to be imported but when imported it is not used and pylint throws an error. * Fixed pylint errors * Apply changes to cli since last merge from master (efc8d49806b14a31d88cfc0f1b6daca1dd373d8d) * Fix lint * Remaining lint errors Co-authored-by: Adrien Ferrand <adferrand@users.noreply.github.com>
-rw-r--r--certbot/CHANGELOG.md2
-rw-r--r--certbot/certbot/_internal/cli.py1581
-rw-r--r--certbot/certbot/_internal/cli/__init__.py526
-rw-r--r--certbot/certbot/_internal/cli/cli_constants.py107
-rw-r--r--certbot/certbot/_internal/cli/cli_utils.py239
-rw-r--r--certbot/certbot/_internal/cli/group_adder.py19
-rw-r--r--certbot/certbot/_internal/cli/helpful.py468
-rw-r--r--certbot/certbot/_internal/cli/paths_parser.py50
-rw-r--r--certbot/certbot/_internal/cli/plugins_parsing.py97
-rw-r--r--certbot/certbot/_internal/cli/report_config_interaction.py27
-rw-r--r--certbot/certbot/_internal/cli/subparsers.py72
-rw-r--r--certbot/certbot/_internal/cli/verb_help.py106
-rw-r--r--certbot/tests/cli_test.py2
-rw-r--r--certbot/tests/helpful_test.py193
14 files changed, 1906 insertions, 1583 deletions
diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md
index ff3061e01..126b07eec 100644
--- a/certbot/CHANGELOG.md
+++ b/certbot/CHANGELOG.md
@@ -13,7 +13,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Changed
-*
+* certbot._internal.cli is now a package split in submodules instead of a whole module.
### Fixed
diff --git a/certbot/certbot/_internal/cli.py b/certbot/certbot/_internal/cli.py
deleted file mode 100644
index c0dbf7424..000000000
--- a/certbot/certbot/_internal/cli.py
+++ /dev/null
@@ -1,1581 +0,0 @@
-"""Certbot command line argument & config processing."""
-# pylint: disable=too-many-lines
-from __future__ import print_function
-
-import argparse
-import copy
-import glob
-import logging.handlers
-import sys
-
-import configargparse
-import six
-import zope.component
-import zope.interface
-from zope.interface import interfaces as zope_interfaces
-
-from acme import challenges
-from acme.magic_typing import Any
-from acme.magic_typing import Dict
-from acme.magic_typing import Optional
-import certbot
-from certbot import crypto_util
-from certbot import errors
-from certbot import interfaces
-from certbot import util
-from certbot._internal import constants
-from certbot._internal import hooks
-from certbot._internal.plugins import disco as plugins_disco
-import certbot._internal.plugins.selection as plugin_selection
-from certbot.compat import os
-from certbot.display import util as display_util
-import certbot.plugins.enhancements as enhancements
-
-logger = logging.getLogger(__name__)
-
-# Global, to save us from a lot of argument passing within the scope of this module
-helpful_parser = None # type: Optional[HelpfulArgumentParser]
-
-# 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"])
-
-old_path_fragment = os.path.join(".local", "share", "letsencrypt")
-new_path_prefix = os.path.abspath(os.path.join(os.sep, "opt",
- "eff.org", "certbot", "venv"))
-if old_path_fragment in sys.argv[0] or sys.argv[0].startswith(new_path_prefix):
- cli_command = LEAUTO
-else:
- cli_command = "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
-certificate. """.format(cli_command)
-
-# This section is used for --help and --help all ; it needs information
-# about installed plugins to be fully formatted
-COMMAND_OVERVIEW = """The most common SUBCOMMANDS and flags are:
-
-obtain, install, and renew certificates:
- (default) run Obtain & install a certificate in your current webserver
- certonly Obtain or renew a certificate, but do not install it
- renew Renew all previously obtained certificates that are near expiry
- enhance Add security enhancements to your existing configuration
- -d DOMAINS Comma-separated list of domains to obtain a certificate for
-
- %s
- --standalone Run a standalone webserver for authentication
- %s
- --webroot Place files in a server's webroot folder for authentication
- --manual Obtain certificates interactively, or using shell script hooks
-
- -n Run non-interactively
- --test-cert Obtain a test certificate from a staging server
- --dry-run Test "renew" or "certonly" without saving any certificates to disk
-
-manage certificates:
- certificates Display information about certificates you have from Certbot
- revoke Revoke a certificate (supply --cert-name or --cert-path)
- delete Delete a certificate (supply --cert-name)
-
-manage your account:
- register Create an ACME account
- unregister Deactivate an ACME account
- update_account Update an ACME account
- --agree-tos Agree to the ACME server's Subscriber Agreement
- -m EMAIL Email address for important account notifications
-"""
-
-# This is the short help for certbot --help, where we disable argparse
-# altogether
-HELP_AND_VERSION_USAGE = """
-More detailed help:
-
- -h, --help [TOPIC] print this message, or detailed help on a topic;
- the available TOPICS are:
-
- all, automation, commands, paths, security, testing, or any of the
- subcommands or plugins (certonly, renew, install, register, nginx,
- apache, standalone, webroot, etc.)
- -h all print a detailed help page including all topics
- --version print the version number
-"""
-
-
-# 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",)),
- "renew_hook": set(("deploy_hook",)),
- "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 (string_types)
- :param modifiers: config options that modify modified
- :type modifiers: iterable or str (string_types)
-
- """
- if isinstance(modified, six.string_types):
- modified = (modified,)
- if isinstance(modifiers, six.string_types):
- modifiers = (modifiers,)
-
- for var in modified:
- VAR_MODIFIERS.setdefault(var, set()).update(modifiers)
-
-
-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 # type: ignore
- if detector is None and helpful_parser is not 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( # type: ignore
- plugins, reconstructed_args, detect_defaults=True)
- # propagate plugin requests: eg --standalone modifies config.authenticator
- detector.authenticator, detector.installer = ( # type: ignore
- plugin_selection.cli_plugin_requests(detector))
-
- if not isinstance(getattr(detector, var), _Default):
- logger.debug("Var %s=%s (set by user).", var, getattr(detector, var))
- return True
-
- for modifier in VAR_MODIFIERS.get(var, []):
- if set_by_cli(modifier):
- logger.debug("Var %s=%s (set by user).",
- var, VAR_MODIFIERS.get(var, []))
- return True
-
- return False
-
-# static housekeeping var
-# functions attributed are not supported by mypy
-# https://github.com/python/mypy/issues/2087
-set_by_cli.detector = None # type: ignore
-
-
-def has_default_value(option, value):
- """Does option have the default value?
-
- If the default value of option is not known, False is returned.
-
- :param str option: configuration variable being considered
- :param value: value of the configuration variable named option
-
- :returns: True if option has the default value, otherwise, False
- :rtype: bool
-
- """
- if helpful_parser is not None:
- return (option in helpful_parser.defaults and
- helpful_parser.defaults[option] == value)
- return False
-
-
-def option_was_set(option, value):
- """Was option set by the user or does it differ from the default?
-
- :param str option: configuration variable being considered
- :param value: value of the configuration variable named option
-
- :returns: True if the option was set, otherwise, False
- :rtype: bool
-
- """
- return set_by_cli(option) or not has_default_value(option, value)
-
-
-def argparse_type(variable):
- """Return our argparse type function for a config variable (default: str)"""
- # pylint: disable=protected-access
- if helpful_parser is not None:
- 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)
- with open(filename, mode) as the_file:
- contents = the_file.read()
- return filename, contents
- 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 copy.deepcopy(constants.CLI_DEFAULTS[name])
-
-
-def config_help(name, hidden=False):
- """Extract the help message for an `.IConfig` attribute."""
- if hidden:
- return argparse.SUPPRESS
- field = interfaces.IConfig.__getitem__(name) # type: zope.interface.interface.Attribute
- return field.__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 CustomHelpFormatter(argparse.HelpFormatter):
- """This is a clone of ArgumentDefaultsHelpFormatter, with bugfixes.
-
- In particular we fix https://bugs.python.org/issue28742
- """
-
- def _get_help_string(self, action):
- helpstr = action.help
- if '%(default)' not in action.help and '(default:' not in action.help:
- if action.default != argparse.SUPPRESS:
- defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
- if action.option_strings or action.nargs in defaulting_nargs:
- helpstr += ' (default: %(default)s)'
- return helpstr
-
-# The attributes here are:
-# short: a string that will be displayed by "certbot -h commands"
-# opts: a string that heads the section of flags with which this command is documented,
-# both for "certbot -h SUBCOMMAND" and "certbot -h all"
-# usage: an optional string that overrides the header of "certbot -h SUBCOMMAND"
-VERB_HELP = [
- ("run (default)", {
- "short": "Obtain/renew a certificate, and install it",
- "opts": "Options for obtaining & installing certificates",
- "usage": SHORT_USAGE.replace("[SUBCOMMAND]", ""),
- "realname": "run"
- }),
- ("certonly", {
- "short": "Obtain or renew a certificate, but do not install it",
- "opts": "Options for modifying how a certificate is obtained",
- "usage": ("\n\n certbot certonly [options] [-d DOMAIN] [-d DOMAIN] ...\n\n"
- "This command obtains a TLS/SSL certificate without installing it anywhere.")
- }),
- ("renew", {
- "short": "Renew all certificates (or one specified with --cert-name)",
- "opts": ("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"
- " https://certbot.eff.org/docs/using.html#renewal for more"
- " information on these."),
- "usage": "\n\n certbot renew [--cert-name CERTNAME] [options]\n\n"
- }),
- ("certificates", {
- "short": "List certificates managed by Certbot",
- "opts": "List certificates managed by Certbot",
- "usage": ("\n\n certbot certificates [options] ...\n\n"
- "Print information about the status of certificates managed by Certbot.")
- }),
- ("delete", {
- "short": "Clean up all files related to a certificate",
- "opts": "Options for deleting a certificate",
- "usage": "\n\n certbot delete --cert-name CERTNAME\n\n"
- }),
- ("revoke", {
- "short": "Revoke a certificate specified with --cert-path or --cert-name",
- "opts": "Options for revocation of certificates",
- "usage": "\n\n certbot revoke [--cert-path /path/to/fullchain.pem | "
- "--cert-name example.com] [options]\n\n"
- }),
- ("register", {
- "short": "Register for account with Let's Encrypt / other ACME server",
- "opts": "Options for account registration",
- "usage": "\n\n certbot register --email user@example.com [options]\n\n"
- }),
- ("update_account", {
- "short": "Update existing account with Let's Encrypt / other ACME server",
- "opts": "Options for account modification",
- "usage": "\n\n certbot update_account --email updated_email@example.com [options]\n\n"
- }),
- ("unregister", {
- "short": "Irrevocably deactivate your account",
- "opts": "Options for account deactivation.",
- "usage": "\n\n certbot unregister [options]\n\n"
- }),
- ("install", {
- "short": "Install an arbitrary certificate in a server",
- "opts": "Options for modifying how a certificate is deployed",
- "usage": "\n\n certbot install --cert-path /path/to/fullchain.pem "
- " --key-path /path/to/private-key [options]\n\n"
- }),
- ("rollback", {
- "short": "Roll back server conf changes made during certificate installation",
- "opts": "Options for rolling back server configuration changes",
- "usage": "\n\n certbot rollback --checkpoints 3 [options]\n\n"
- }),
- ("plugins", {
- "short": "List plugins that are installed and available on your system",
- "opts": 'Options for the "plugins" subcommand',
- "usage": "\n\n certbot plugins [options]\n\n"
- }),
- ("update_symlinks", {
- "short": "Recreate symlinks in your /etc/letsencrypt/live/ directory",
- "opts": ("Recreates certificate and key symlinks in {0}, if you changed them by hand "
- "or edited a renewal configuration file".format(
- os.path.join(flag_default("config_dir"), "live"))),
- "usage": "\n\n certbot update_symlinks [options]\n\n"
- }),
- ("enhance", {
- "short": "Add security enhancements to your existing configuration",
- "opts": ("Helps to harden the TLS configuration by adding security enhancements "
- "to already existing configuration."),
- "usage": "\n\n certbot enhance [options]\n\n"
- }),
-
-]
-# VERB_HELP is a list in order to preserve order, but a dict is sometimes useful
-VERB_HELP_MAP = dict(VERB_HELP)
-
-
-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._internal import main
- self.VERBS = {
- "auth": main.certonly,
- "certonly": main.certonly,
- "run": main.run,
- "install": main.install,
- "plugins": main.plugins_cmd,
- "register": main.register,
- "update_account": main.update_account,
- "unregister": main.unregister,
- "renew": main.renew,
- "revoke": main.revoke,
- "rollback": main.rollback,
- "everything": main.run,
- "update_symlinks": main.update_symlinks,
- "certificates": main.certificates,
- "delete": main.delete,
- "enhance": main.enhance,
- }
-
- # Get notification function for printing
- try:
- self.notify = zope.component.getUtility(
- interfaces.IDisplay).notification
- except zope_interfaces.ComponentLookupError:
- self.notify = display_util.NoninteractiveDisplay(
- sys.stdout).notification
-
-
- # List of topics for which additional help can be provided
- HELP_TOPICS = ["all", "security", "paths", "automation", "testing"]
- HELP_TOPICS += list(self.VERBS) + self.COMMANDS_TOPICS + ["manage"]
-
- plugin_names = list(plugins)
- self.help_topics = HELP_TOPICS + plugin_names + [None] # type: ignore
-
- self.detect_defaults = detect_defaults
- self.args = args
-
- if self.args and self.args[0] == 'help':
- self.args[0] = '--help'
-
- self.determine_verb()
- help1 = self.prescan_for_flag("-h", self.help_topics)
- help2 = self.prescan_for_flag("--help", self.help_topics)
- if isinstance(help1, bool) and isinstance(help2, bool):
- self.help_arg = help1 or help2
- else:
- self.help_arg = help1 if isinstance(help1, six.string_types) else help2
-
- short_usage = self._usage_string(plugins, self.help_arg)
-
- self.visible_topics = self.determine_help_topics(self.help_arg)
-
- # elements are added by .add_group()
- self.groups = {} # type: Dict[str, argparse._ArgumentGroup]
- # elements are added by .parse_args()
- self.defaults = {} # type: Dict[str, Any]
-
- self.parser = configargparse.ArgParser(
- prog="certbot",
- usage=short_usage,
- formatter_class=CustomHelpFormatter,
- args_for_setting_config_path=["-c", "--config"],
- default_config_files=flag_default("config_files"),
- config_arg_help_message="path to config file (default: {0})".format(
- " and ".join(flag_default("config_files"))))
-
- # This is the only way to turn off overly verbose config flag documentation
- self.parser._add_config_file_help = False
-
- # Help that are synonyms for --help subcommands
- COMMANDS_TOPICS = ["command", "commands", "subcommand", "subcommands", "verbs"]
- def _list_subcommands(self):
- longest = max(len(v) for v in VERB_HELP_MAP)
-
- text = "The full list of available SUBCOMMANDS is:\n\n"
- for verb, props in sorted(VERB_HELP):
- doc = props.get("short", "")
- text += '{0:<{length}} {1}\n'.format(verb, doc, length=longest)
-
- text += "\nYou can get more help on a specific subcommand with --help SUBCOMMAND\n"
- return text
-
- def _usage_string(self, plugins, help_arg):
- """Make usage strings late so that plugins can be initialised late
-
- :param plugins: all discovered plugins
- :param help_arg: False for none; True for --help; "TOPIC" for --help TOPIC
- :rtype: str
- :returns: a short usage string for the top of --help TOPIC)
- """
- if "nginx" in plugins:
- nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
- else:
- nginx_doc = "(the certbot nginx plugin is not installed)"
- if "apache" in plugins:
- apache_doc = "--apache Use the Apache plugin for authentication & installation"
- else:
- apache_doc = "(the certbot apache plugin is not installed)"
-
- usage = SHORT_USAGE
- if help_arg is True:
- self.notify(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_AND_VERSION_USAGE)
- sys.exit(0)
- elif help_arg in self.COMMANDS_TOPICS:
- self.notify(usage + self._list_subcommands())
- sys.exit(0)
- elif help_arg == "all":
- # if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at
- # the top; if we're doing --help someothertopic, it's OT so it's not
- usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc)
- else:
- custom = VERB_HELP_MAP.get(help_arg, {}).get("usage", None)
- usage = custom if custom else usage
-
- return usage
-
- def remove_config_file_domains_for_renewal(self, parsed_args):
- """Make "certbot renew" safe if domains are set in cli.ini."""
- # Works around https://github.com/certbot/certbot/issues/4096
- if self.verb == "renew":
- for source, flags in self.parser._source_to_settings.items(): # pylint: disable=protected-access
- if source.startswith("config_file") and "domains" in flags:
- parsed_args.domains = _Default() if self.detect_defaults else []
-
- 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
-
- self.remove_config_file_domains_for_renewal(parsed_args)
-
- if self.detect_defaults:
- return parsed_args
-
- self.defaults = dict((key, copy.deepcopy(self.parser.get_default(key)))
- for key in vars(parsed_args))
-
- # Do any post-parsing homework here
-
- if self.verb == "renew":
- if parsed_args.force_interactive:
- raise errors.Error(
- "{0} cannot be used with renew".format(
- constants.FORCE_INTERACTIVE_FLAG))
- parsed_args.noninteractive_mode = True
-
- if parsed_args.force_interactive and parsed_args.noninteractive_mode:
- raise errors.Error(
- "Flag for non-interactive mode and {0} conflict".format(
- constants.FORCE_INTERACTIVE_FLAG))
-
- if parsed_args.staging or parsed_args.dry_run:
- self.set_test_server(parsed_args)
-
- if parsed_args.csr:
- self.handle_csr(parsed_args)
-
- if parsed_args.must_staple:
- parsed_args.staple = True
-
- if parsed_args.validate_hooks:
- hooks.validate_hooks(parsed_args)
-
- if parsed_args.allow_subset_of_names:
- if any(util.is_wildcard_domain(d) for d in parsed_args.domains):
- raise errors.Error("Using --allow-subset-of-names with a"
- " wildcard domain is not supported.")
-
- if parsed_args.hsts and parsed_args.auto_hsts:
- raise errors.Error(
- "Parameters --hsts and --auto-hsts cannot be used simultaneously.")
-
- return parsed_args
-
- def set_test_server(self, parsed_args):
- """We have --staging/--dry-run; perform sanity check and set config.server"""
-
- # Flag combinations should produce these results:
- # | --staging | --dry-run |
- # ------------------------------------------------------------
- # | --server acme-v02 | Use staging | Use staging |
- # | --server acme-staging-v02 | Use staging | Use staging |
- # | --server <other> | Conflict error | Use <other> |
-
- default_servers = (flag_default("server"), constants.STAGING_URI)
-
- if parsed_args.staging and parsed_args.server not in default_servers:
- raise errors.Error("--server value conflicts with --staging")
-
- if parsed_args.server in default_servers:
- 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
-
- 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.")
- if parsed_args.allow_subset_of_names:
- raise errors.Error("--allow-subset-of-names cannot be used with --csr")
-
- csrfile, contents = parsed_args.csr[0:2]
- typ, csr, domains = crypto_util.import_csr_file(csrfile, contents)
-
- # 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 = {d.lower() for d in domains}
- config_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, topics, *args, **kwargs):
- """Add a new command line argument.
-
- :param topics: str or [str] help topic(s) this should be listed under,
- or None for options that don't fit under a specific
- topic which will only be shown in "--help all" output.
- The first entry determines where the flag lives in the
- "--help all" output (None -> "optional arguments").
- :param list *args: the names of this argument flag
- :param dict **kwargs: various argparse settings for this argument
-
- """
-
- if isinstance(topics, list):
- # if this flag can be listed in multiple sections, try to pick the one
- # that the user has asked for help about
- topic = self.help_arg if self.help_arg in topics else topics[0]
- else:
- topic = topics # there's only one
-
- 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.
-
- """
- util.add_deprecated_argument(
- self.parser.add_argument, argument_name, num_args)
-
- def add_group(self, topic, verbs=(), **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.
- :param str verbs: List of subcommands that should be documented as part of
- this help group / topic
-
- :returns: The new argument group.
- :rtype: `HelpfulArgumentGroup`
-
- """
- if self.visible_topics[topic]:
- self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)
- if self.help_arg:
- for v in verbs:
- self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v]["short"])
- 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.long_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":
- # Addition of condition closes #6209 (removal of duplicate route53 option).
- return {t: t != 'certbot-route53:auth' for t in self.help_topics}
- elif not chosen_topic:
- return {t: False for t in self.help_topics}
- return {t: t == chosen_topic for t in self.help_topics}
-
-
-def _add_all_groups(helpful):
- helpful.add_group("automation", description="Flags for automating execution & other tweaks")
- helpful.add_group("security", description="Security parameters & server settings")
- helpful.add_group("testing",
- description="The following flags are meant for testing and integration purposes only.")
- helpful.add_group("paths", description="Flags for changing execution paths & servers")
- helpful.add_group("manage",
- description="Various subcommands and flags are available for managing your certificates:",
- verbs=["certificates", "delete", "renew", "revoke", "update_symlinks"])
-
- # VERBS
- for verb, docs in VERB_HELP:
- name = docs.get("realname", verb)
- helpful.add_group(name, description=docs["opts"])
-
-
-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)
- _add_all_groups(helpful)
-
- # --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",
- default=flag_default("text_mode"), help=argparse.SUPPRESS)
- helpful.add(
- None, "--max-log-backups", type=nonnegative_int,
- default=flag_default("max_log_backups"),
- help="Specifies the maximum number of backup logs that should "
- "be kept by Certbot's built in log rotation. Setting this "
- "flag to 0 disables log rotation entirely, causing "
- "Certbot to always append to the same log file.")
- helpful.add(
- [None, "automation", "run", "certonly", "enhance"],
- "-n", "--non-interactive", "--noninteractive",
- dest="noninteractive_mode", action="store_true",
- default=flag_default("noninteractive_mode"),
- 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, "register", "run", "certonly", "enhance"],
- constants.FORCE_INTERACTIVE_FLAG, action="store_true",
- default=flag_default("force_interactive"),
- help="Force Certbot to be interactive even if it detects it's not "
- "being run in a terminal. This flag cannot be used with the "
- "renew subcommand.")
- helpful.add(
- [None, "run", "certonly", "certificates", "enhance"],
- "-d", "--domains", "--domain", dest="domains",
- metavar="DOMAIN", action=_DomainsAction,
- default=flag_default("domains"),
- 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. The first domain provided will be the "
- "subject CN of the certificate, and all domains will be "
- "Subject Alternative Names on the certificate. "
- "The first domain will also be used in "
- "some software user interfaces and as the file paths for the "
- "certificate and related material unless otherwise "
- "specified or you already have a certificate with the same "
- "name. In the case of a name collision it will append a number "
- "like 0001 to the file path name. (default: Ask)")
- helpful.add(
- [None, "run", "certonly", "register"],
- "--eab-kid", dest="eab_kid",
- metavar="EAB_KID",
- help="Key Identifier for External Account Binding"
- )
- helpful.add(
- [None, "run", "certonly", "register"],
- "--eab-hmac-key", dest="eab_hmac_key",
- metavar="EAB_HMAC_KEY",
- help="HMAC key for External Account Binding"
- )
- helpful.add(
- [None, "run", "certonly", "manage", "delete", "certificates",
- "renew", "enhance"], "--cert-name", dest="certname",
- metavar="CERTNAME", default=flag_default("certname"),
- help="Certificate name to apply. This name is used by Certbot for housekeeping "
- "and in file paths; it doesn't affect the content of the certificate itself. "
- "To see certificate names, run 'certbot certificates'. "
- "When creating a new certificate, specifies the new certificate's name. "
- "(default: the first provided domain or the name of an existing "
- "certificate on your system for the same domains)")
- helpful.add(
- [None, "testing", "renew", "certonly"],
- "--dry-run", action="store_true", dest="dry_run",
- default=flag_default("dry_run"),
- help="Perform a test run of the client, obtaining test (invalid) certificates"
- " 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 certificates, 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. --deploy-hook commands are not called.")
- helpful.add(
- ["register", "automation"], "--register-unsafely-without-email", action="store_true",
- default=flag_default("register_unsafely_without_email"),
- 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(
- ["register", "update_account", "unregister", "automation"], "-m", "--email",
- default=flag_default("email"),
- help=config_help("email"))
- helpful.add(["register", "update_account", "automation"], "--eff-email", action="store_true",
- default=flag_default("eff_email"), dest="eff_email",
- help="Share your e-mail address with EFF")
- helpful.add(["register", "update_account", "automation"], "--no-eff-email",
- action="store_false", default=flag_default("eff_email"), dest="eff_email",
- help="Don't share your e-mail address with EFF")
- helpful.add(
- ["automation", "certonly", "run"],
- "--keep-until-expiring", "--keep", "--reinstall",
- dest="reinstall", action="store_true", default=flag_default("reinstall"),
- help="If the requested certificate matches an existing certificate, always keep the "
- "existing one until it is due for renewal (for the "
- "'run' subcommand this means reinstall the existing certificate). (default: Ask)")
- helpful.add(
- "automation", "--expand", action="store_true", default=flag_default("expand"),
- help="If an existing certificate is a strict subset of the requested names, "
- "always expand and replace it with the additional names. (default: Ask)")
- helpful.add(
- "automation", "--version", action="version",
- version="%(prog)s {0}".format(certbot.__version__),
- help="show program's version number and exit")
- helpful.add(
- ["automation", "renew"],
- "--force-renewal", "--renew-by-default", dest="renew_by_default",
- action="store_true", default=flag_default("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", "--renew-with-new-domains", dest="renew_with_new_domains",
- action="store_true", default=flag_default("renew_with_new_domains"),
- help="If a "
- "certificate already exists for the requested certificate name "
- "but does not match the requested domains, renew it now, "
- "regardless of whether it is near expiry.")
- helpful.add(
- "automation", "--reuse-key", dest="reuse_key",
- action="store_true", default=flag_default("reuse_key"),
- help="When renewing, use the same private key as the existing "
- "certificate.")
-
- helpful.add(
- ["automation", "renew", "certonly"],
- "--allow-subset-of-names", action="store_true",
- default=flag_default("allow_subset_of_names"),
- 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",
- default=flag_default("tos"),
- help="Agree to the ACME Subscriber Agreement (default: Ask)")
- helpful.add(
- ["unregister", "automation"], "--account", metavar="ACCOUNT_ID",
- default=flag_default("account"),
- help="Account ID to use")
- helpful.add(
- "automation", "--duplicate", dest="duplicate", action="store_true",
- default=flag_default("duplicate"),
- 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",
- default=flag_default("os_packages_only"),
- help="(certbot-auto only) install OS package dependencies and then stop")
- helpful.add(
- "automation", "--no-self-upgrade", action="store_true",
- default=flag_default("no_self_upgrade"),
- help="(certbot-auto only) prevent the certbot-auto script from"
- " upgrading itself to newer released versions (default: Upgrade"
- " automatically)")
- helpful.add(
- "automation", "--no-bootstrap", action="store_true",
- default=flag_default("no_bootstrap"),
- help="(certbot-auto only) prevent the certbot-auto script from"
- " installing OS-level dependencies (default: Prompt to install "
- " OS-wide dependencies, but exit if the user says 'No')")
- helpful.add(
- "automation", "--no-permissions-check", action="store_true",
- default=flag_default("no_permissions_check"),
- help="(certbot-auto only) skip the check on the file system"
- " permissions of the certbot-auto script")
- helpful.add(
- ["automation", "renew", "certonly", "run"],
- "-q", "--quiet", dest="quiet", action="store_true",
- default=flag_default("quiet"),
- help="Silence all output except errors. Useful for automation via cron."
- " Implies --non-interactive.")
- # overwrites server, handled in HelpfulArgumentParser.parse_args()
- helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging",
- dest="staging", action="store_true", default=flag_default("staging"),
- help="Use the staging server to obtain or revoke test (invalid) certificates; equivalent"
- " to --server " + constants.STAGING_URI)
- helpful.add(
- "testing", "--debug", action="store_true", default=flag_default("debug"),
- help="Show tracebacks in case of errors, and allow certbot-auto "
- "execution on experimental platforms")
- helpful.add(
- [None, "certonly", "run"], "--debug-challenges", action="store_true",
- default=flag_default("debug_challenges"),
- help="After setting up challenges, wait for user input before "
- "submitting to CA")
- helpful.add(
- "testing", "--no-verify-ssl", action="store_true",
- help=config_help("no_verify_ssl"),
- default=flag_default("no_verify_ssl"))
- helpful.add(
- ["testing", "standalone", "manual"], "--http-01-port", type=int,
- dest="http01_port",
- default=flag_default("http01_port"), help=config_help("http01_port"))
- helpful.add(
- ["testing", "standalone"], "--http-01-address",
- dest="http01_address",
- default=flag_default("http01_address"), help=config_help("http01_address"))
- helpful.add(
- ["testing", "nginx"], "--https-port", type=int,
- default=flag_default("https_port"),
- help=config_help("https_port"))
- helpful.add(
- "testing", "--break-my-certs", action="store_true",
- default=flag_default("break_my_certs"),
- help="Be willing to replace or renew valid certificates with invalid "
- "(testing/staging) certificates")
- 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",
- dest="must_staple", default=flag_default("must_staple"),
- help=config_help("must_staple"))
- helpful.add(
- ["security", "enhance"],
- "--redirect", action="store_true", dest="redirect",
- default=flag_default("redirect"),
- help="Automatically redirect all HTTP traffic to HTTPS for the newly "
- "authenticated vhost. (default: Ask)")
- helpful.add(
- "security", "--no-redirect", action="store_false", dest="redirect",
- default=flag_default("redirect"),
- help="Do not automatically redirect all HTTP traffic to HTTPS for the newly "
- "authenticated vhost. (default: Ask)")
- helpful.add(
- ["security", "enhance"],
- "--hsts", action="store_true", dest="hsts", default=flag_default("hsts"),
- help="Add the Strict-Transport-Security header to every HTTP response."
- " Forcing browser to always use SSL for the domain."
- " Defends against SSL Stripping.")
- helpful.add(
- "security", "--no-hsts", action="store_false", dest="hsts",
- default=flag_default("hsts"), help=argparse.SUPPRESS)
- helpful.add(
- ["security", "enhance"],
- "--uir", action="store_true", dest="uir", default=flag_default("uir"),
- help='Add the "Content-Security-Policy: upgrade-insecure-requests"'
- ' header to every HTTP response. Forcing the browser to use'
- ' https:// for every http:// resource.')
- helpful.add(
- "security", "--no-uir", action="store_false", dest="uir", default=flag_default("uir"),
- help=argparse.SUPPRESS)
- helpful.add(
- "security", "--staple-ocsp", action="store_true", dest="staple",
- default=flag_default("staple"),
- help="Enables OCSP Stapling. A valid OCSP response is stapled to"
- " the certificate that the server offers during TLS.")
- helpful.add(
- "security", "--no-staple-ocsp", action="store_false", dest="staple",
- default=flag_default("staple"), help=argparse.SUPPRESS)
- helpful.add(
- "security", "--strict-permissions", action="store_true",
- default=flag_default("strict_permissions"),
- help="Require that all configuration files are owned by the current "
- "user; only needed if your config is somewhere unsafe like /tmp/")
- helpful.add(
- ["manual", "standalone", "certonly", "renew"],
- "--preferred-challenges", dest="pref_challs",
- action=_PrefChallAction, default=flag_default("pref_challs"),
- help='A sorted, comma delimited list of the preferred challenge to '
- 'use during authorization with the most preferred challenge '
- 'listed first (Eg, "dns" or "http,dns"). '
- 'Not all plugins support all challenges. See '
- 'https://certbot.eff.org/docs/using.html#plugins for details. '
- 'ACME Challenges are versioned, but if you pick "http" rather '
- 'than "http-01", Certbot will select the latest version '
- 'automatically.')
- 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. When renewing several certificates that have"
- " identical pre-hooks, only the first will be executed.")
- 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. This is only"
- " run if an attempt was made to obtain/renew a certificate. If"
- " multiple renewed certificates have identical post-hooks, only"
- " one will be run.")
- helpful.add("renew", "--renew-hook",
- action=_RenewHookAction, help=argparse.SUPPRESS)
- helpful.add(
- "renew", "--no-random-sleep-on-renew", action="store_false",
- default=flag_default("random_sleep_on_renew"), dest="random_sleep_on_renew",
- help=argparse.SUPPRESS)
- helpful.add(
- "renew", "--deploy-hook", action=_DeployHookAction,
- help='Command to be run in a shell once for each successfully'
- ' issued certificate. For this command, the shell variable'
- ' $RENEWED_LINEAGE will point to the config live subdirectory'
- ' (for example, "/etc/letsencrypt/live/example.com") containing'
- ' the new certificates and keys; the shell variable'
- ' $RENEWED_DOMAINS will contain a space-delimited list of'
- ' renewed certificate domains (for example, "example.com'
- ' www.example.com"')
- helpful.add(
- "renew", "--disable-hook-validation",
- action="store_false", dest="validate_hooks",
- default=flag_default("validate_hooks"),
- help="Ordinarily the commands specified for"
- " --pre-hook/--post-hook/--deploy-hook will be checked for"
- " validity, to see if the programs being run are in the $PATH,"
- " so that mistakes can be caught early, even when the hooks"
- " aren't being run just yet. The validation is rather"
- " simplistic and fails if you use more advanced shell"
- " constructs, so you can use this switch to disable it."
- " (default: False)")
- helpful.add(
- "renew", "--no-directory-hooks", action="store_false",
- default=flag_default("directory_hooks"), dest="directory_hooks",
- help="Disable running executables found in Certbot's hook directories"
- " during renewal. (default: False)")
- helpful.add(
- "renew", "--disable-renew-updates", action="store_true",
- default=flag_default("disable_renew_updates"), dest="disable_renew_updates",
- help="Disable automatic updates to your server configuration that"
- " would otherwise be done by the selected installer plugin, and triggered"
- " when the user executes \"certbot renew\", regardless of if the certificate"
- " is renewed. This setting does not apply to important TLS configuration"
- " updates.")
- helpful.add(
- "renew", "--no-autorenew", action="store_false",
- default=flag_default("autorenew"), dest="autorenew",
- help="Disable auto renewal of certificates.")
-
- # Populate the command line parameters for new style enhancements
- enhancements.populate_cli(helpful.add)
-
- _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):
- from certbot._internal.client import sample_user_agent # avoid import loops
- helpful.add(
- None, "--user-agent", default=flag_default("user_agent"),
- 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, '
- 'plugin and use case, and to know when to deprecate support for past Python '
- "versions and flags. If you wish to hide this information from the Let's "
- 'Encrypt server, set this to "". '
- '(default: {0}). The flags encoded in the user agent are: '
- '--duplicate, --force-renew, --allow-subset-of-names, -n, and '
- 'whether any hooks are set.'.format(sample_user_agent()))
- helpful.add(
- None, "--user-agent-comment", default=flag_default("user_agent_comment"),
- type=_user_agent_comment_type,
- help="Add a comment to the default user agent string. May be used when repackaging Certbot "
- "or calling it from another tool to allow additional statistical data to be collected."
- " Ignored if --user-agent is set. (Example: Foo-Wrapper/1.0)")
- helpful.add("certonly",
- "--csr", default=flag_default("csr"), type=read_file,
- help="Path to a Certificate Signing Request (CSR) in DER or PEM format."
- " Currently --csr only works with the 'certonly' subcommand.")
- helpful.add("revoke",
- "--reason", dest="reason",
- choices=CaseInsensitiveList(sorted(constants.REVOCATION_REASONS,
- key=constants.REVOCATION_REASONS.get)),
- action=_EncodeReasonAction, default=flag_default("reason"),
- help="Specify reason for revoking certificate. (default: unspecified)")
- helpful.add("revoke",
- "--delete-after-revoke", action="store_true",
- default=flag_default("delete_after_revoke"),
- help="Delete certificates after revoking them, along with all previous and later "
- "versions of those certificates.")
- helpful.add("revoke",
- "--no-delete-after-revoke", action="store_false",
- dest="delete_after_revoke",
- default=flag_default("delete_after_revoke"),
- help="Do not delete certificates after revoking them. This "
- "option should be used with caution because the 'renew' "
- "subcommand will attempt to renew undeleted revoked "
- "certificates.")
- 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", default=flag_default("init"),
- help="Initialize plugins.")
- helpful.add("plugins",
- "--prepare", action="store_true", default=flag_default("prepare"),
- help="Initialize and prepare plugins.")
- helpful.add("plugins",
- "--authenticators", action="append_const", dest="ifaces",
- default=flag_default("ifaces"),
- const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.")
- helpful.add("plugins",
- "--installers", action="append_const", dest="ifaces",
- default=flag_default("ifaces"),
- const=interfaces.IInstaller, help="Limit to installer plugins only.")
-
-
-class CaseInsensitiveList(list):
- """A list that will ignore case when searching.
-
- This class is passed to the `choices` argument of `argparse.add_arguments`
- through the `helpful` wrapper. It is necessary due to special handling of
- command line arguments by `set_by_cli` in which the `type_func` is not applied."""
- def __contains__(self, element):
- return super(CaseInsensitiveList, self).__contains__(element.lower())
-
-
-def _paths_parser(helpful):
- add = helpful.add
- verb = helpful.verb
- if verb == "help":
- verb = helpful.help_arg
-
- cph = "Path to where certificate is saved (with auth --csr), installed from, or revoked."
- sections = ["paths", "install", "revoke", "certonly", "manage"]
- if verb == "certonly":
- add(sections, "--cert-path", type=os.path.abspath,
- default=flag_default("auth_cert_path"), help=cph)
- elif verb == "revoke":
- add(sections, "--cert-path", type=read_file, required=False, help=cph)
- else:
- add(sections, "--cert-path", type=os.path.abspath, help=cph)
-
- 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",
- type=((verb == "revoke" and read_file) or os.path.abspath),
- help="Path to private key for certificate installation "
- "or revocation (if account key is missing)")
-
- default_cp = None
- if verb == "certonly":
- default_cp = flag_default("auth_chain_path")
- add(["paths", "install"], "--fullchain-path", default=default_cp, type=os.path.abspath,
- help="Accompanying path to a full certificate chain (certificate 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"))
-
-
-def _plugins_parsing(helpful, plugins):
- # It's nuts, but there are two "plugins" topics. Somehow this works
- helpful.add_group(
- "plugins", description="Plugin Selection: 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", "--configurator", default=flag_default("configurator"),
- help="Name of the plugin that is both an authenticator and an installer."
- " Should not be used together with --authenticator or --installer. "
- "(default: Ask)")
- helpful.add("plugins", "-a", "--authenticator", default=flag_default("authenticator"),
- help="Authenticator plugin name.")
- helpful.add("plugins", "-i", "--installer", default=flag_default("installer"),
- help="Installer plugin name (also used to find domains).")
- helpful.add(["plugins", "certonly", "run", "install"],
- "--apache", action="store_true", default=flag_default("apache"),
- help="Obtain and install certificates using Apache")
- helpful.add(["plugins", "certonly", "run", "install"],
- "--nginx", action="store_true", default=flag_default("nginx"),
- help="Obtain and install certificates using Nginx")
- helpful.add(["plugins", "certonly"], "--standalone", action="store_true",
- default=flag_default("standalone"),
- help='Obtain certificates using a "standalone" webserver.')
- helpful.add(["plugins", "certonly"], "--manual", action="store_true",
- default=flag_default("manual"),
- help="Provide laborious manual instructions for obtaining a certificate")
- helpful.add(["plugins", "certonly"], "--webroot", action="store_true",
- default=flag_default("webroot"),
- help="Obtain certificates by placing files in a webroot directory.")
- helpful.add(["plugins", "certonly"], "--dns-cloudflare", action="store_true",
- default=flag_default("dns_cloudflare"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using Cloudflare for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-cloudxns", action="store_true",
- default=flag_default("dns_cloudxns"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using CloudXNS for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-digitalocean", action="store_true",
- default=flag_default("dns_digitalocean"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using DigitalOcean for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-dnsimple", action="store_true",
- default=flag_default("dns_dnsimple"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using DNSimple for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-dnsmadeeasy", action="store_true",
- default=flag_default("dns_dnsmadeeasy"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using DNS Made Easy for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-gehirn", action="store_true",
- default=flag_default("dns_gehirn"),
- help=("Obtain certificates using a DNS TXT record "
- "(if you are using Gehirn Infrastructure Service for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-google", action="store_true",
- default=flag_default("dns_google"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using Google Cloud DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-linode", action="store_true",
- default=flag_default("dns_linode"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using Linode for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-luadns", action="store_true",
- default=flag_default("dns_luadns"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using LuaDNS for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true",
- default=flag_default("dns_nsone"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using NS1 for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-ovh", action="store_true",
- default=flag_default("dns_ovh"),
- help=("Obtain certificates using a DNS TXT record (if you are "
- "using OVH for DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-rfc2136", action="store_true",
- default=flag_default("dns_rfc2136"),
- help="Obtain certificates using a DNS TXT record (if you are using BIND for DNS).")
- helpful.add(["plugins", "certonly"], "--dns-route53", action="store_true",
- default=flag_default("dns_route53"),
- help=("Obtain certificates using a DNS TXT record (if you are using Route53 for "
- "DNS)."))
- helpful.add(["plugins", "certonly"], "--dns-sakuracloud", action="store_true",
- default=flag_default("dns_sakuracloud"),
- help=("Obtain certificates using a DNS TXT record "
- "(if you are using Sakura Cloud for DNS)."))
-
- # 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 _EncodeReasonAction(argparse.Action):
- """Action class for parsing revocation reason."""
-
- def __call__(self, parser, namespace, reason, option_string=None):
- """Encodes the reason for certificate revocation."""
- code = constants.REVOCATION_REASONS[reason.lower()]
- setattr(namespace, self.dest, code)
-
-
-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 = 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
-
-class _PrefChallAction(argparse.Action):
- """Action class for parsing preferred challenges."""
-
- def __call__(self, parser, namespace, pref_challs, option_string=None):
- try:
- challs = parse_preferred_challenges(pref_challs.split(","))
- except errors.Error as error:
- raise argparse.ArgumentError(self, str(error))
- namespace.pref_challs.extend(challs)
-
-
-def parse_preferred_challenges(pref_challs):
- """Translate and validate preferred challenges.
-
- :param pref_challs: list of preferred challenge types
- :type pref_challs: `list` of `str`
-
- :returns: validated list of preferred challenge types
- :rtype: `list` of `str`
-
- :raises errors.Error: if pref_challs is invalid
-
- """
- aliases = {"dns": "dns-01", "http": "http-01"}
- challs = [c.strip() for c in pref_challs]
- challs = [aliases.get(c, c) for c in challs]
-
- unrecognized = ", ".join(name for name in challs
- if name not in challenges.Challenge.TYPES)
- if unrecognized:
- raise errors.Error(
- "Unrecognized challenges: {0}".format(unrecognized))
- return challs
-
-
-def _user_agent_comment_type(value):
- if "(" in value or ")" in value:
- raise argparse.ArgumentTypeError("may not contain parentheses")
- return value
-
-
-class _DeployHookAction(argparse.Action):
- """Action class for parsing deploy hooks."""
-
- def __call__(self, parser, namespace, values, option_string=None):
- renew_hook_set = namespace.deploy_hook != namespace.renew_hook
- if renew_hook_set and namespace.renew_hook != values:
- raise argparse.ArgumentError(
- self, "conflicts with --renew-hook value")
- namespace.deploy_hook = namespace.renew_hook = values
-
-
-class _RenewHookAction(argparse.Action):
- """Action class for parsing renew hooks."""
-
- def __call__(self, parser, namespace, values, option_string=None):
- deploy_hook_set = namespace.deploy_hook is not None
- if deploy_hook_set and namespace.deploy_hook != values:
- raise argparse.ArgumentError(
- self, "conflicts with --deploy-hook value")
- namespace.renew_hook = values
-
-
-def nonnegative_int(value):
- """Converts value to an int and checks that it is not negative.
-
- This function should used as the type parameter for argparse
- arguments.
-
- :param str value: value provided on the command line
-
- :returns: integer representation of value
- :rtype: int
-
- :raises argparse.ArgumentTypeError: if value isn't a non-negative integer
-
- """
- try:
- int_value = int(value)
- except ValueError:
- raise argparse.ArgumentTypeError("value must be an integer")
-
- if int_value < 0:
- raise argparse.ArgumentTypeError("value must be non-negative")
- return int_value
diff --git a/certbot/certbot/_internal/cli/__init__.py b/certbot/certbot/_internal/cli/__init__.py
new file mode 100644
index 000000000..96dfb163e
--- /dev/null
+++ b/certbot/certbot/_internal/cli/__init__.py
@@ -0,0 +1,526 @@
+"""Certbot command line argument & config processing."""
+# pylint: disable=too-many-lines
+from __future__ import print_function
+import logging
+import logging.handlers
+import argparse
+import sys
+import certbot._internal.plugins.selection as plugin_selection
+from certbot._internal.plugins import disco as plugins_disco
+
+from acme.magic_typing import Optional
+
+# pylint: disable=ungrouped-imports
+import certbot
+from certbot._internal import constants
+
+import certbot.plugins.enhancements as enhancements
+
+
+from certbot._internal.cli.cli_constants import (
+ LEAUTO,
+ old_path_fragment,
+ new_path_prefix,
+ cli_command,
+ SHORT_USAGE,
+ COMMAND_OVERVIEW,
+ HELP_AND_VERSION_USAGE,
+ ARGPARSE_PARAMS_TO_REMOVE,
+ EXIT_ACTIONS,
+ ZERO_ARG_ACTIONS,
+ VAR_MODIFIERS
+)
+
+from certbot._internal.cli.cli_utils import (
+ _Default,
+ read_file,
+ flag_default,
+ config_help,
+ HelpfulArgumentGroup,
+ CustomHelpFormatter,
+ _DomainsAction,
+ add_domains,
+ CaseInsensitiveList,
+ _user_agent_comment_type,
+ _EncodeReasonAction,
+ parse_preferred_challenges,
+ _PrefChallAction,
+ _DeployHookAction,
+ _RenewHookAction,
+ nonnegative_int
+)
+
+# These imports depend on cli_constants and cli_utils.
+from certbot._internal.cli.report_config_interaction import report_config_interaction
+from certbot._internal.cli.verb_help import VERB_HELP, VERB_HELP_MAP
+from certbot._internal.cli.group_adder import _add_all_groups
+from certbot._internal.cli.subparsers import _create_subparsers
+from certbot._internal.cli.paths_parser import _paths_parser
+from certbot._internal.cli.plugins_parsing import _plugins_parsing
+
+# These imports depend on some or all of the submodules for cli.
+from certbot._internal.cli.helpful import HelpfulArgumentParser
+# pylint: enable=ungrouped-imports
+
+
+logger = logging.getLogger(__name__)
+
+
+# Global, to save us from a lot of argument passing within the scope of this module
+helpful_parser = None # type: Optional[HelpfulArgumentParser]
+
+
+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)
+ _add_all_groups(helpful)
+
+ # --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",
+ default=flag_default("text_mode"), help=argparse.SUPPRESS)
+ helpful.add(
+ None, "--max-log-backups", type=nonnegative_int,
+ default=flag_default("max_log_backups"),
+ help="Specifies the maximum number of backup logs that should "
+ "be kept by Certbot's built in log rotation. Setting this "
+ "flag to 0 disables log rotation entirely, causing "
+ "Certbot to always append to the same log file.")
+ helpful.add(
+ [None, "automation", "run", "certonly", "enhance"],
+ "-n", "--non-interactive", "--noninteractive",
+ dest="noninteractive_mode", action="store_true",
+ default=flag_default("noninteractive_mode"),
+ 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, "register", "run", "certonly", "enhance"],
+ constants.FORCE_INTERACTIVE_FLAG, action="store_true",
+ default=flag_default("force_interactive"),
+ help="Force Certbot to be interactive even if it detects it's not "
+ "being run in a terminal. This flag cannot be used with the "
+ "renew subcommand.")
+ helpful.add(
+ [None, "run", "certonly", "certificates", "enhance"],
+ "-d", "--domains", "--domain", dest="domains",
+ metavar="DOMAIN", action=_DomainsAction,
+ default=flag_default("domains"),
+ 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. The first domain provided will be the "
+ "subject CN of the certificate, and all domains will be "
+ "Subject Alternative Names on the certificate. "
+ "The first domain will also be used in "
+ "some software user interfaces and as the file paths for the "
+ "certificate and related material unless otherwise "
+ "specified or you already have a certificate with the same "
+ "name. In the case of a name collision it will append a number "
+ "like 0001 to the file path name. (default: Ask)")
+ helpful.add(
+ [None, "run", "certonly", "register"],
+ "--eab-kid", dest="eab_kid",
+ metavar="EAB_KID",
+ help="Key Identifier for External Account Binding"
+ )
+ helpful.add(
+ [None, "run", "certonly", "register"],
+ "--eab-hmac-key", dest="eab_hmac_key",
+ metavar="EAB_HMAC_KEY",
+ help="HMAC key for External Account Binding"
+ )
+ helpful.add(
+ [None, "run", "certonly", "manage", "delete", "certificates",
+ "renew", "enhance"], "--cert-name", dest="certname",
+ metavar="CERTNAME", default=flag_default("certname"),
+ help="Certificate name to apply. This name is used by Certbot for housekeeping "
+ "and in file paths; it doesn't affect the content of the certificate itself. "
+ "To see certificate names, run 'certbot certificates'. "
+ "When creating a new certificate, specifies the new certificate's name. "
+ "(default: the first provided domain or the name of an existing "
+ "certificate on your system for the same domains)")
+ helpful.add(
+ [None, "testing", "renew", "certonly"],
+ "--dry-run", action="store_true", dest="dry_run",
+ default=flag_default("dry_run"),
+ help="Perform a test run of the client, obtaining test (invalid) certificates"
+ " 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 certificates, 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. --deploy-hook commands are not called.")
+ helpful.add(
+ ["register", "automation"], "--register-unsafely-without-email", action="store_true",
+ default=flag_default("register_unsafely_without_email"),
+ 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(
+ ["register", "update_account", "unregister", "automation"], "-m", "--email",
+ default=flag_default("email"),
+ help=config_help("email"))
+ helpful.add(["register", "update_account", "automation"], "--eff-email", action="store_true",
+ default=flag_default("eff_email"), dest="eff_email",
+ help="Share your e-mail address with EFF")
+ helpful.add(["register", "update_account", "automation"], "--no-eff-email",
+ action="store_false", default=flag_default("eff_email"), dest="eff_email",
+ help="Don't share your e-mail address with EFF")
+ helpful.add(
+ ["automation", "certonly", "run"],
+ "--keep-until-expiring", "--keep", "--reinstall",
+ dest="reinstall", action="store_true", default=flag_default("reinstall"),
+ help="If the requested certificate matches an existing certificate, always keep the "
+ "existing one until it is due for renewal (for the "
+ "'run' subcommand this means reinstall the existing certificate). (default: Ask)")
+ helpful.add(
+ "automation", "--expand", action="store_true", default=flag_default("expand"),
+ help="If an existing certificate is a strict subset of the requested names, "
+ "always expand and replace it with the additional names. (default: Ask)")
+ helpful.add(
+ "automation", "--version", action="version",
+ version="%(prog)s {0}".format(certbot.__version__),
+ help="show program's version number and exit")
+ helpful.add(
+ ["automation", "renew"],
+ "--force-renewal", "--renew-by-default", dest="renew_by_default",
+ action="store_true", default=flag_default("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", "--renew-with-new-domains", dest="renew_with_new_domains",
+ action="store_true", default=flag_default("renew_with_new_domains"),
+ help="If a "
+ "certificate already exists for the requested certificate name "
+ "but does not match the requested domains, renew it now, "
+ "regardless of whether it is near expiry.")
+ helpful.add(
+ "automation", "--reuse-key", dest="reuse_key",
+ action="store_true", default=flag_default("reuse_key"),
+ help="When renewing, use the same private key as the existing "
+ "certificate.")
+
+ helpful.add(
+ ["automation", "renew", "certonly"],
+ "--allow-subset-of-names", action="store_true",
+ default=flag_default("allow_subset_of_names"),
+ 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",
+ default=flag_default("tos"),
+ help="Agree to the ACME Subscriber Agreement (default: Ask)")
+ helpful.add(
+ ["unregister", "automation"], "--account", metavar="ACCOUNT_ID",
+ default=flag_default("account"),
+ help="Account ID to use")
+ helpful.add(
+ "automation", "--duplicate", dest="duplicate", action="store_true",
+ default=flag_default("duplicate"),
+ 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",
+ default=flag_default("os_packages_only"),
+ help="(certbot-auto only) install OS package dependencies and then stop")
+ helpful.add(
+ "automation", "--no-self-upgrade", action="store_true",
+ default=flag_default("no_self_upgrade"),
+ help="(certbot-auto only) prevent the certbot-auto script from"
+ " upgrading itself to newer released versions (default: Upgrade"
+ " automatically)")
+ helpful.add(
+ "automation", "--no-bootstrap", action="store_true",
+ default=flag_default("no_bootstrap"),
+ help="(certbot-auto only) prevent the certbot-auto script from"
+ " installing OS-level dependencies (default: Prompt to install "
+ " OS-wide dependencies, but exit if the user says 'No')")
+ helpful.add(
+ "automation", "--no-permissions-check", action="store_true",
+ default=flag_default("no_permissions_check"),
+ help="(certbot-auto only) skip the check on the file system"
+ " permissions of the certbot-auto script")
+ helpful.add(
+ ["automation", "renew", "certonly", "run"],
+ "-q", "--quiet", dest="quiet", action="store_true",
+ default=flag_default("quiet"),
+ help="Silence all output except errors. Useful for automation via cron."
+ " Implies --non-interactive.")
+ # overwrites server, handled in HelpfulArgumentParser.parse_args()
+ helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging",
+ dest="staging", action="store_true", default=flag_default("staging"),
+ help="Use the staging server to obtain or revoke test (invalid) certificates; equivalent"
+ " to --server " + constants.STAGING_URI)
+ helpful.add(
+ "testing", "--debug", action="store_true", default=flag_default("debug"),
+ help="Show tracebacks in case of errors, and allow certbot-auto "
+ "execution on experimental platforms")
+ helpful.add(
+ [None, "certonly", "run"], "--debug-challenges", action="store_true",
+ default=flag_default("debug_challenges"),
+ help="After setting up challenges, wait for user input before "
+ "submitting to CA")
+ helpful.add(
+ "testing", "--no-verify-ssl", action="store_true",
+ help=config_help("no_verify_ssl"),
+ default=flag_default("no_verify_ssl"))
+ helpful.add(
+ ["testing", "standalone", "manual"], "--http-01-port", type=int,
+ dest="http01_port",
+ default=flag_default("http01_port"), help=config_help("http01_port"))
+ helpful.add(
+ ["testing", "standalone"], "--http-01-address",
+ dest="http01_address",
+ default=flag_default("http01_address"), help=config_help("http01_address"))
+ helpful.add(
+ ["testing", "nginx"], "--https-port", type=int,
+ default=flag_default("https_port"),
+ help=config_help("https_port"))
+ helpful.add(
+ "testing", "--break-my-certs", action="store_true",
+ default=flag_default("break_my_certs"),
+ help="Be willing to replace or renew valid certificates with invalid "
+ "(testing/staging) certificates")
+ 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",
+ dest="must_staple", default=flag_default("must_staple"),
+ help=config_help("must_staple"))
+ helpful.add(
+ ["security", "enhance"],
+ "--redirect", action="store_true", dest="redirect",
+ default=flag_default("redirect"),
+ help="Automatically redirect all HTTP traffic to HTTPS for the newly "
+ "authenticated vhost. (default: Ask)")
+ helpful.add(
+ "security", "--no-redirect", action="store_false", dest="redirect",
+ default=flag_default("redirect"),
+ help="Do not automatically redirect all HTTP traffic to HTTPS for the newly "
+ "authenticated vhost. (default: Ask)")
+ helpful.add(
+ ["security", "enhance"],
+ "--hsts", action="store_true", dest="hsts", default=flag_default("hsts"),
+ help="Add the Strict-Transport-Security header to every HTTP response."
+ " Forcing browser to always use SSL for the domain."
+ " Defends against SSL Stripping.")
+ helpful.add(
+ "security", "--no-hsts", action="store_false", dest="hsts",
+ default=flag_default("hsts"), help=argparse.SUPPRESS)
+ helpful.add(
+ ["security", "enhance"],
+ "--uir", action="store_true", dest="uir", default=flag_default("uir"),
+ help='Add the "Content-Security-Policy: upgrade-insecure-requests"'
+ ' header to every HTTP response. Forcing the browser to use'
+ ' https:// for every http:// resource.')
+ helpful.add(
+ "security", "--no-uir", action="store_false", dest="uir", default=flag_default("uir"),
+ help=argparse.SUPPRESS)
+ helpful.add(
+ "security", "--staple-ocsp", action="store_true", dest="staple",
+ default=flag_default("staple"),
+ help="Enables OCSP Stapling. A valid OCSP response is stapled to"
+ " the certificate that the server offers during TLS.")
+ helpful.add(
+ "security", "--no-staple-ocsp", action="store_false", dest="staple",
+ default=flag_default("staple"), help=argparse.SUPPRESS)
+ helpful.add(
+ "security", "--strict-permissions", action="store_true",
+ default=flag_default("strict_permissions"),
+ help="Require that all configuration files are owned by the current "
+ "user; only needed if your config is somewhere unsafe like /tmp/")
+ helpful.add(
+ ["manual", "standalone", "certonly", "renew"],
+ "--preferred-challenges", dest="pref_challs",
+ action=_PrefChallAction, default=flag_default("pref_challs"),
+ help='A sorted, comma delimited list of the preferred challenge to '
+ 'use during authorization with the most preferred challenge '
+ 'listed first (Eg, "dns" or "http,dns"). '
+ 'Not all plugins support all challenges. See '
+ 'https://certbot.eff.org/docs/using.html#plugins for details. '
+ 'ACME Challenges are versioned, but if you pick "http" rather '
+ 'than "http-01", Certbot will select the latest version '
+ 'automatically.')
+ 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. When renewing several certificates that have"
+ " identical pre-hooks, only the first will be executed.")
+ 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. This is only"
+ " run if an attempt was made to obtain/renew a certificate. If"
+ " multiple renewed certificates have identical post-hooks, only"
+ " one will be run.")
+ helpful.add("renew", "--renew-hook",
+ action=_RenewHookAction, help=argparse.SUPPRESS)
+ helpful.add(
+ "renew", "--no-random-sleep-on-renew", action="store_false",
+ default=flag_default("random_sleep_on_renew"), dest="random_sleep_on_renew",
+ help=argparse.SUPPRESS)
+ helpful.add(
+ "renew", "--deploy-hook", action=_DeployHookAction,
+ help='Command to be run in a shell once for each successfully'
+ ' issued certificate. For this command, the shell variable'
+ ' $RENEWED_LINEAGE will point to the config live subdirectory'
+ ' (for example, "/etc/letsencrypt/live/example.com") containing'
+ ' the new certificates and keys; the shell variable'
+ ' $RENEWED_DOMAINS will contain a space-delimited list of'
+ ' renewed certificate domains (for example, "example.com'
+ ' www.example.com"')
+ helpful.add(
+ "renew", "--disable-hook-validation",
+ action="store_false", dest="validate_hooks",
+ default=flag_default("validate_hooks"),
+ help="Ordinarily the commands specified for"
+ " --pre-hook/--post-hook/--deploy-hook will be checked for"
+ " validity, to see if the programs being run are in the $PATH,"
+ " so that mistakes can be caught early, even when the hooks"
+ " aren't being run just yet. The validation is rather"
+ " simplistic and fails if you use more advanced shell"
+ " constructs, so you can use this switch to disable it."
+ " (default: False)")
+ helpful.add(
+ "renew", "--no-directory-hooks", action="store_false",
+ default=flag_default("directory_hooks"), dest="directory_hooks",
+ help="Disable running executables found in Certbot's hook directories"
+ " during renewal. (default: False)")
+ helpful.add(
+ "renew", "--disable-renew-updates", action="store_true",
+ default=flag_default("disable_renew_updates"), dest="disable_renew_updates",
+ help="Disable automatic updates to your server configuration that"
+ " would otherwise be done by the selected installer plugin, and triggered"
+ " when the user executes \"certbot renew\", regardless of if the certificate"
+ " is renewed. This setting does not apply to important TLS configuration"
+ " updates.")
+ helpful.add(
+ "renew", "--no-autorenew", action="store_false",
+ default=flag_default("autorenew"), dest="autorenew",
+ help="Disable auto renewal of certificates.")
+
+ # Populate the command line parameters for new style enhancements
+ enhancements.populate_cli(helpful.add)
+
+ _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 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 # type: ignore
+ if detector is None and helpful_parser is not 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( # type: ignore
+ plugins, reconstructed_args, detect_defaults=True)
+ # propagate plugin requests: eg --standalone modifies config.authenticator
+ detector.authenticator, detector.installer = ( # type: ignore
+ plugin_selection.cli_plugin_requests(detector))
+
+ if not isinstance(getattr(detector, var), _Default):
+ logger.debug("Var %s=%s (set by user).", var, getattr(detector, var))
+ return True
+
+ for modifier in VAR_MODIFIERS.get(var, []):
+ if set_by_cli(modifier):
+ logger.debug("Var %s=%s (set by user).",
+ var, VAR_MODIFIERS.get(var, []))
+ return True
+
+ return False
+
+
+# static housekeeping var
+# functions attributed are not supported by mypy
+# https://github.com/python/mypy/issues/2087
+set_by_cli.detector = None # type: ignore
+
+
+def has_default_value(option, value):
+ """Does option have the default value?
+
+ If the default value of option is not known, False is returned.
+
+ :param str option: configuration variable being considered
+ :param value: value of the configuration variable named option
+
+ :returns: True if option has the default value, otherwise, False
+ :rtype: bool
+
+ """
+ if helpful_parser is not None:
+ return (option in helpful_parser.defaults and
+ helpful_parser.defaults[option] == value)
+ return False
+
+
+def option_was_set(option, value):
+ """Was option set by the user or does it differ from the default?
+
+ :param str option: configuration variable being considered
+ :param value: value of the configuration variable named option
+
+ :returns: True if the option was set, otherwise, False
+ :rtype: bool
+
+ """
+ return set_by_cli(option) or not has_default_value(option, value)
+
+
+def argparse_type(variable):
+ """Return our argparse type function for a config variable (default: str)"""
+ # pylint: disable=protected-access
+ if helpful_parser is not None:
+ for action in helpful_parser.parser._actions:
+ if action.type is not None and action.dest == variable:
+ return action.type
+ return str
diff --git a/certbot/certbot/_internal/cli/cli_constants.py b/certbot/certbot/_internal/cli/cli_constants.py
new file mode 100644
index 000000000..748ae0d94
--- /dev/null
+++ b/certbot/certbot/_internal/cli/cli_constants.py
@@ -0,0 +1,107 @@
+"""Certbot command line constants"""
+import sys
+
+from certbot.compat import os
+
+# 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"])
+
+old_path_fragment = os.path.join(".local", "share", "letsencrypt")
+new_path_prefix = os.path.abspath(os.path.join(os.sep, "opt",
+ "eff.org", "certbot", "venv"))
+if old_path_fragment in sys.argv[0] or sys.argv[0].startswith(new_path_prefix):
+ cli_command = LEAUTO
+else:
+ cli_command = "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
+certificate. """.format(cli_command)
+
+# This section is used for --help and --help all ; it needs information
+# about installed plugins to be fully formatted
+COMMAND_OVERVIEW = """The most common SUBCOMMANDS and flags are:
+
+obtain, install, and renew certificates:
+ (default) run Obtain & install a certificate in your current webserver
+ certonly Obtain or renew a certificate, but do not install it
+ renew Renew all previously obtained certificates that are near expiry
+ enhance Add security enhancements to your existing configuration
+ -d DOMAINS Comma-separated list of domains to obtain a certificate for
+
+ %s
+ --standalone Run a standalone webserver for authentication
+ %s
+ --webroot Place files in a server's webroot folder for authentication
+ --manual Obtain certificates interactively, or using shell script hooks
+
+ -n Run non-interactively
+ --test-cert Obtain a test certificate from a staging server
+ --dry-run Test "renew" or "certonly" without saving any certificates to disk
+
+manage certificates:
+ certificates Display information about certificates you have from Certbot
+ revoke Revoke a certificate (supply --cert-name or --cert-path)
+ delete Delete a certificate (supply --cert-name)
+
+manage your account:
+ register Create an ACME account
+ unregister Deactivate an ACME account
+ update_account Update an ACME account
+ --agree-tos Agree to the ACME server's Subscriber Agreement
+ -m EMAIL Email address for important account notifications
+"""
+
+# This is the short help for certbot --help, where we disable argparse
+# altogether
+HELP_AND_VERSION_USAGE = """
+More detailed help:
+
+ -h, --help [TOPIC] print this message, or detailed help on a topic;
+ the available TOPICS are:
+
+ all, automation, commands, paths, security, testing, or any of the
+ subcommands or plugins (certonly, renew, install, register, nginx,
+ apache, standalone, webroot, etc.)
+ -h all print a detailed help page including all topics
+ --version print the version number
+"""
+
+# 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",)),
+ "renew_hook": set(("deploy_hook",)),
+ "server": set(("dry_run", "staging",)),
+ "webroot_map": set(("webroot_path",))}
diff --git a/certbot/certbot/_internal/cli/cli_utils.py b/certbot/certbot/_internal/cli/cli_utils.py
new file mode 100644
index 000000000..a0ddce38f
--- /dev/null
+++ b/certbot/certbot/_internal/cli/cli_utils.py
@@ -0,0 +1,239 @@
+"""Certbot command line util function"""
+import argparse
+import copy
+
+import zope.interface.interface # pylint: disable=unused-import
+
+from acme import challenges
+from certbot import interfaces
+from certbot import util
+from certbot import errors
+from certbot.compat import os
+from certbot._internal import constants
+
+
+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 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)
+ with open(filename, mode) as the_file:
+ contents = the_file.read()
+ return filename, contents
+ 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 copy.deepcopy(constants.CLI_DEFAULTS[name])
+
+
+def config_help(name, hidden=False):
+ """Extract the help message for an `.IConfig` attribute."""
+ if hidden:
+ return argparse.SUPPRESS
+ field = interfaces.IConfig.__getitem__(name) # type: zope.interface.interface.Attribute
+ return field.__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 CustomHelpFormatter(argparse.HelpFormatter):
+ """This is a clone of ArgumentDefaultsHelpFormatter, with bugfixes.
+
+ In particular we fix https://bugs.python.org/issue28742
+ """
+
+ def _get_help_string(self, action):
+ helpstr = action.help
+ if '%(default)' not in action.help and '(default:' not in action.help:
+ if action.default != argparse.SUPPRESS:
+ defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
+ if action.option_strings or action.nargs in defaulting_nargs:
+ helpstr += ' (default: %(default)s)'
+ return helpstr
+
+
+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 = 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
+
+
+class CaseInsensitiveList(list):
+ """A list that will ignore case when searching.
+
+ This class is passed to the `choices` argument of `argparse.add_arguments`
+ through the `helpful` wrapper. It is necessary due to special handling of
+ command line arguments by `set_by_cli` in which the `type_func` is not applied."""
+ def __contains__(self, element):
+ return super(CaseInsensitiveList, self).__contains__(element.lower())
+
+
+def _user_agent_comment_type(value):
+ if "(" in value or ")" in value:
+ raise argparse.ArgumentTypeError("may not contain parentheses")
+ return value
+
+
+class _EncodeReasonAction(argparse.Action):
+ """Action class for parsing revocation reason."""
+
+ def __call__(self, parser, namespace, reason, option_string=None):
+ """Encodes the reason for certificate revocation."""
+ code = constants.REVOCATION_REASONS[reason.lower()]
+ setattr(namespace, self.dest, code)
+
+
+def parse_preferred_challenges(pref_challs):
+ """Translate and validate preferred challenges.
+
+ :param pref_challs: list of preferred challenge types
+ :type pref_challs: `list` of `str`
+
+ :returns: validated list of preferred challenge types
+ :rtype: `list` of `str`
+
+ :raises errors.Error: if pref_challs is invalid
+
+ """
+ aliases = {"dns": "dns-01", "http": "http-01"}
+ challs = [c.strip() for c in pref_challs]
+ challs = [aliases.get(c, c) for c in challs]
+
+ unrecognized = ", ".join(name for name in challs
+ if name not in challenges.Challenge.TYPES)
+ if unrecognized:
+ raise errors.Error(
+ "Unrecognized challenges: {0}".format(unrecognized))
+ return challs
+
+
+class _PrefChallAction(argparse.Action):
+ """Action class for parsing preferred challenges."""
+
+ def __call__(self, parser, namespace, pref_challs, option_string=None):
+ try:
+ challs = parse_preferred_challenges(pref_challs.split(","))
+ except errors.Error as error:
+ raise argparse.ArgumentError(self, str(error))
+ namespace.pref_challs.extend(challs)
+
+
+class _DeployHookAction(argparse.Action):
+ """Action class for parsing deploy hooks."""
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ renew_hook_set = namespace.deploy_hook != namespace.renew_hook
+ if renew_hook_set and namespace.renew_hook != values:
+ raise argparse.ArgumentError(
+ self, "conflicts with --renew-hook value")
+ namespace.deploy_hook = namespace.renew_hook = values
+
+
+class _RenewHookAction(argparse.Action):
+ """Action class for parsing renew hooks."""
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ deploy_hook_set = namespace.deploy_hook is not None
+ if deploy_hook_set and namespace.deploy_hook != values:
+ raise argparse.ArgumentError(
+ self, "conflicts with --deploy-hook value")
+ namespace.renew_hook = values
+
+
+def nonnegative_int(value):
+ """Converts value to an int and checks that it is not negative.
+
+ This function should used as the type parameter for argparse
+ arguments.
+
+ :param str value: value provided on the command line
+
+ :returns: integer representation of value
+ :rtype: int
+
+ :raises argparse.ArgumentTypeError: if value isn't a non-negative integer
+
+ """
+ try:
+ int_value = int(value)
+ except ValueError:
+ raise argparse.ArgumentTypeError("value must be an integer")
+
+ if int_value < 0:
+ raise argparse.ArgumentTypeError("value must be non-negative")
+ return int_value
diff --git a/certbot/certbot/_internal/cli/group_adder.py b/certbot/certbot/_internal/cli/group_adder.py
new file mode 100644
index 000000000..f22fbc496
--- /dev/null
+++ b/certbot/certbot/_internal/cli/group_adder.py
@@ -0,0 +1,19 @@
+"""This module contains a function to add the groups of arguments for the help
+display"""
+from certbot._internal.cli import VERB_HELP
+
+
+def _add_all_groups(helpful):
+ helpful.add_group("automation", description="Flags for automating execution & other tweaks")
+ helpful.add_group("security", description="Security parameters & server settings")
+ helpful.add_group("testing",
+ description="The following flags are meant for testing and integration purposes only.")
+ helpful.add_group("paths", description="Flags for changing execution paths & servers")
+ helpful.add_group("manage",
+ description="Various subcommands and flags are available for managing your certificates:",
+ verbs=["certificates", "delete", "renew", "revoke", "update_symlinks"])
+
+ # VERBS
+ for verb, docs in VERB_HELP:
+ name = docs.get("realname", verb)
+ helpful.add_group(name, description=docs["opts"])
diff --git a/certbot/certbot/_internal/cli/helpful.py b/certbot/certbot/_internal/cli/helpful.py
new file mode 100644
index 000000000..e63ab4b87
--- /dev/null
+++ b/certbot/certbot/_internal/cli/helpful.py
@@ -0,0 +1,468 @@
+"""Certbot command line argument parser"""
+from __future__ import print_function
+import argparse
+import copy
+import glob
+import sys
+import configargparse
+import six
+import zope.component
+import zope.interface
+
+from zope.interface import interfaces as zope_interfaces
+
+# pylint: disable=unused-import, no-name-in-module
+from acme.magic_typing import Any, Dict, Optional
+# pylint: enable=unused-import, no-name-in-module
+
+from certbot import crypto_util
+from certbot import errors
+from certbot import interfaces
+from certbot import util
+from certbot.compat import os
+from certbot._internal import constants
+from certbot._internal import hooks
+
+from certbot.display import util as display_util
+
+from certbot._internal.cli import (
+ SHORT_USAGE,
+ CustomHelpFormatter,
+ flag_default,
+ VERB_HELP,
+ VERB_HELP_MAP,
+ COMMAND_OVERVIEW,
+ HELP_AND_VERSION_USAGE,
+ _Default,
+ add_domains,
+ EXIT_ACTIONS,
+ ZERO_ARG_ACTIONS,
+ ARGPARSE_PARAMS_TO_REMOVE,
+ HelpfulArgumentGroup
+)
+
+
+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._internal import main
+ self.VERBS = {
+ "auth": main.certonly,
+ "certonly": main.certonly,
+ "run": main.run,
+ "install": main.install,
+ "plugins": main.plugins_cmd,
+ "register": main.register,
+ "update_account": main.update_account,
+ "unregister": main.unregister,
+ "renew": main.renew,
+ "revoke": main.revoke,
+ "rollback": main.rollback,
+ "everything": main.run,
+ "update_symlinks": main.update_symlinks,
+ "certificates": main.certificates,
+ "delete": main.delete,
+ "enhance": main.enhance,
+ }
+
+ # Get notification function for printing
+ try:
+ self.notify = zope.component.getUtility(
+ interfaces.IDisplay).notification
+ except zope_interfaces.ComponentLookupError:
+ self.notify = display_util.NoninteractiveDisplay(
+ sys.stdout).notification
+
+
+ # List of topics for which additional help can be provided
+ HELP_TOPICS = ["all", "security", "paths", "automation", "testing"]
+ HELP_TOPICS += list(self.VERBS) + self.COMMANDS_TOPICS + ["manage"]
+
+ plugin_names = list(plugins)
+ self.help_topics = HELP_TOPICS + plugin_names + [None] # type: ignore
+
+ self.detect_defaults = detect_defaults
+ self.args = args
+
+ if self.args and self.args[0] == 'help':
+ self.args[0] = '--help'
+
+ self.determine_verb()
+ help1 = self.prescan_for_flag("-h", self.help_topics)
+ help2 = self.prescan_for_flag("--help", self.help_topics)
+ if isinstance(help1, bool) and isinstance(help2, bool):
+ self.help_arg = help1 or help2
+ else:
+ self.help_arg = help1 if isinstance(help1, six.string_types) else help2
+
+ short_usage = self._usage_string(plugins, self.help_arg)
+
+ self.visible_topics = self.determine_help_topics(self.help_arg)
+
+ # elements are added by .add_group()
+ self.groups = {} # type: Dict[str, argparse._ArgumentGroup]
+ # elements are added by .parse_args()
+ self.defaults = {} # type: Dict[str, Any]
+
+ self.parser = configargparse.ArgParser(
+ prog="certbot",
+ usage=short_usage,
+ formatter_class=CustomHelpFormatter,
+ args_for_setting_config_path=["-c", "--config"],
+ default_config_files=flag_default("config_files"),
+ config_arg_help_message="path to config file (default: {0})".format(
+ " and ".join(flag_default("config_files"))))
+
+ # This is the only way to turn off overly verbose config flag documentation
+ self.parser._add_config_file_help = False
+
+ # Help that are synonyms for --help subcommands
+ COMMANDS_TOPICS = ["command", "commands", "subcommand", "subcommands", "verbs"]
+
+ def _list_subcommands(self):
+ longest = max(len(v) for v in VERB_HELP_MAP)
+
+ text = "The full list of available SUBCOMMANDS is:\n\n"
+ for verb, props in sorted(VERB_HELP):
+ doc = props.get("short", "")
+ text += '{0:<{length}} {1}\n'.format(verb, doc, length=longest)
+
+ text += "\nYou can get more help on a specific subcommand with --help SUBCOMMAND\n"
+ return text
+
+ def _usage_string(self, plugins, help_arg):
+ """Make usage strings late so that plugins can be initialised late
+
+ :param plugins: all discovered plugins
+ :param help_arg: False for none; True for --help; "TOPIC" for --help TOPIC
+ :rtype: str
+ :returns: a short usage string for the top of --help TOPIC)
+ """
+ if "nginx" in plugins:
+ nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
+ else:
+ nginx_doc = "(the certbot nginx plugin is not installed)"
+ if "apache" in plugins:
+ apache_doc = "--apache Use the Apache plugin for authentication & installation"
+ else:
+ apache_doc = "(the certbot apache plugin is not installed)"
+
+ usage = SHORT_USAGE
+ if help_arg is True:
+ self.notify(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_AND_VERSION_USAGE)
+ sys.exit(0)
+ elif help_arg in self.COMMANDS_TOPICS:
+ self.notify(usage + self._list_subcommands())
+ sys.exit(0)
+ elif help_arg == "all":
+ # if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at
+ # the top; if we're doing --help someothertopic, it's OT so it's not
+ usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc)
+ else:
+ custom = VERB_HELP_MAP.get(help_arg, {}).get("usage", None)
+ usage = custom if custom else usage
+
+ return usage
+
+ def remove_config_file_domains_for_renewal(self, parsed_args):
+ """Make "certbot renew" safe if domains are set in cli.ini."""
+ # Works around https://github.com/certbot/certbot/issues/4096
+ if self.verb == "renew":
+ for source, flags in self.parser._source_to_settings.items(): # pylint: disable=protected-access
+ if source.startswith("config_file") and "domains" in flags:
+ parsed_args.domains = _Default() if self.detect_defaults else []
+
+ 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
+
+ self.remove_config_file_domains_for_renewal(parsed_args)
+
+ if self.detect_defaults:
+ return parsed_args
+
+ self.defaults = dict((key, copy.deepcopy(self.parser.get_default(key)))
+ for key in vars(parsed_args))
+
+ # Do any post-parsing homework here
+
+ if self.verb == "renew":
+ if parsed_args.force_interactive:
+ raise errors.Error(
+ "{0} cannot be used with renew".format(
+ constants.FORCE_INTERACTIVE_FLAG))
+ parsed_args.noninteractive_mode = True
+
+ if parsed_args.force_interactive and parsed_args.noninteractive_mode:
+ raise errors.Error(
+ "Flag for non-interactive mode and {0} conflict".format(
+ constants.FORCE_INTERACTIVE_FLAG))
+
+ if parsed_args.staging or parsed_args.dry_run:
+ self.set_test_server(parsed_args)
+
+ if parsed_args.csr:
+ self.handle_csr(parsed_args)
+
+ if parsed_args.must_staple:
+ parsed_args.staple = True
+
+ if parsed_args.validate_hooks:
+ hooks.validate_hooks(parsed_args)
+
+ if parsed_args.allow_subset_of_names:
+ if any(util.is_wildcard_domain(d) for d in parsed_args.domains):
+ raise errors.Error("Using --allow-subset-of-names with a"
+ " wildcard domain is not supported.")
+
+ if parsed_args.hsts and parsed_args.auto_hsts:
+ raise errors.Error(
+ "Parameters --hsts and --auto-hsts cannot be used simultaneously.")
+
+ return parsed_args
+
+ def set_test_server(self, parsed_args):
+ """We have --staging/--dry-run; perform sanity check and set config.server"""
+
+ # Flag combinations should produce these results:
+ # | --staging | --dry-run |
+ # ------------------------------------------------------------
+ # | --server acme-v02 | Use staging | Use staging |
+ # | --server acme-staging-v02 | Use staging | Use staging |
+ # | --server <other> | Conflict error | Use <other> |
+
+ default_servers = (flag_default("server"), constants.STAGING_URI)
+
+ if parsed_args.staging and parsed_args.server not in default_servers:
+ raise errors.Error("--server value conflicts with --staging")
+
+ if parsed_args.server in default_servers:
+ 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
+
+ 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.")
+ if parsed_args.allow_subset_of_names:
+ raise errors.Error("--allow-subset-of-names cannot be used with --csr")
+
+ csrfile, contents = parsed_args.csr[0:2]
+ typ, csr, domains = crypto_util.import_csr_file(csrfile, contents)
+
+ # 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 = {d.lower() for d in domains}
+ config_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, topics, *args, **kwargs):
+ """Add a new command line argument.
+
+ :param topics: str or [str] help topic(s) this should be listed under,
+ or None for options that don't fit under a specific
+ topic which will only be shown in "--help all" output.
+ The first entry determines where the flag lives in the
+ "--help all" output (None -> "optional arguments").
+ :param list *args: the names of this argument flag
+ :param dict **kwargs: various argparse settings for this argument
+
+ """
+
+ if isinstance(topics, list):
+ # if this flag can be listed in multiple sections, try to pick the one
+ # that the user has asked for help about
+ topic = self.help_arg if self.help_arg in topics else topics[0]
+ else:
+ topic = topics # there's only one
+
+ 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.
+
+ """
+ util.add_deprecated_argument(
+ self.parser.add_argument, argument_name, num_args)
+
+ def add_group(self, topic, verbs=(), **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.
+ :param str verbs: List of subcommands that should be documented as part of
+ this help group / topic
+
+ :returns: The new argument group.
+ :rtype: `HelpfulArgumentGroup`
+
+ """
+ if self.visible_topics[topic]:
+ self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)
+ if self.help_arg:
+ for v in verbs:
+ self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v]["short"])
+ 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.long_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":
+ # Addition of condition closes #6209 (removal of duplicate route53 option).
+ return {t: t != 'certbot-route53:auth' for t in self.help_topics}
+ elif not chosen_topic:
+ return {t: False for t in self.help_topics}
+ return {t: t == chosen_topic for t in self.help_topics}
diff --git a/certbot/certbot/_internal/cli/paths_parser.py b/certbot/certbot/_internal/cli/paths_parser.py
new file mode 100644
index 000000000..4378435d7
--- /dev/null
+++ b/certbot/certbot/_internal/cli/paths_parser.py
@@ -0,0 +1,50 @@
+"""This is a module that adds configuration to the argument parser regarding
+paths for certificates"""
+from certbot.compat import os
+from certbot._internal.cli import (
+ read_file,
+ flag_default,
+ config_help
+)
+
+
+def _paths_parser(helpful):
+ add = helpful.add
+ verb = helpful.verb
+ if verb == "help":
+ verb = helpful.help_arg
+
+ cph = "Path to where certificate is saved (with auth --csr), installed from, or revoked."
+ sections = ["paths", "install", "revoke", "certonly", "manage"]
+ if verb == "certonly":
+ add(sections, "--cert-path", type=os.path.abspath,
+ default=flag_default("auth_cert_path"), help=cph)
+ elif verb == "revoke":
+ add(sections, "--cert-path", type=read_file, required=False, help=cph)
+ else:
+ add(sections, "--cert-path", type=os.path.abspath, help=cph)
+
+ 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",
+ type=((verb == "revoke" and read_file) or os.path.abspath),
+ help="Path to private key for certificate installation "
+ "or revocation (if account key is missing)")
+
+ default_cp = None
+ if verb == "certonly":
+ default_cp = flag_default("auth_chain_path")
+ add(["paths", "install"], "--fullchain-path", default=default_cp, type=os.path.abspath,
+ help="Accompanying path to a full certificate chain (certificate 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"))
diff --git a/certbot/certbot/_internal/cli/plugins_parsing.py b/certbot/certbot/_internal/cli/plugins_parsing.py
new file mode 100644
index 000000000..9e11ad3ab
--- /dev/null
+++ b/certbot/certbot/_internal/cli/plugins_parsing.py
@@ -0,0 +1,97 @@
+"""This is a module that handles parsing of plugins for the argument parser"""
+from certbot._internal.cli import flag_default
+
+
+def _plugins_parsing(helpful, plugins):
+ # It's nuts, but there are two "plugins" topics. Somehow this works
+ helpful.add_group(
+ "plugins", description="Plugin Selection: 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", "--configurator", default=flag_default("configurator"),
+ help="Name of the plugin that is both an authenticator and an installer."
+ " Should not be used together with --authenticator or --installer. "
+ "(default: Ask)")
+ helpful.add("plugins", "-a", "--authenticator", default=flag_default("authenticator"),
+ help="Authenticator plugin name.")
+ helpful.add("plugins", "-i", "--installer", default=flag_default("installer"),
+ help="Installer plugin name (also used to find domains).")
+ helpful.add(["plugins", "certonly", "run", "install"],
+ "--apache", action="store_true", default=flag_default("apache"),
+ help="Obtain and install certificates using Apache")
+ helpful.add(["plugins", "certonly", "run", "install"],
+ "--nginx", action="store_true", default=flag_default("nginx"),
+ help="Obtain and install certificates using Nginx")
+ helpful.add(["plugins", "certonly"], "--standalone", action="store_true",
+ default=flag_default("standalone"),
+ help='Obtain certificates using a "standalone" webserver.')
+ helpful.add(["plugins", "certonly"], "--manual", action="store_true",
+ default=flag_default("manual"),
+ help="Provide laborious manual instructions for obtaining a certificate")
+ helpful.add(["plugins", "certonly"], "--webroot", action="store_true",
+ default=flag_default("webroot"),
+ help="Obtain certificates by placing files in a webroot directory.")
+ helpful.add(["plugins", "certonly"], "--dns-cloudflare", action="store_true",
+ default=flag_default("dns_cloudflare"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using Cloudflare for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-cloudxns", action="store_true",
+ default=flag_default("dns_cloudxns"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using CloudXNS for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-digitalocean", action="store_true",
+ default=flag_default("dns_digitalocean"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using DigitalOcean for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-dnsimple", action="store_true",
+ default=flag_default("dns_dnsimple"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using DNSimple for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-dnsmadeeasy", action="store_true",
+ default=flag_default("dns_dnsmadeeasy"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using DNS Made Easy for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-gehirn", action="store_true",
+ default=flag_default("dns_gehirn"),
+ help=("Obtain certificates using a DNS TXT record "
+ "(if you are using Gehirn Infrastructure Service for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-google", action="store_true",
+ default=flag_default("dns_google"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using Google Cloud DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-linode", action="store_true",
+ default=flag_default("dns_linode"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using Linode for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-luadns", action="store_true",
+ default=flag_default("dns_luadns"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using LuaDNS for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-nsone", action="store_true",
+ default=flag_default("dns_nsone"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using NS1 for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-ovh", action="store_true",
+ default=flag_default("dns_ovh"),
+ help=("Obtain certificates using a DNS TXT record (if you are "
+ "using OVH for DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-rfc2136", action="store_true",
+ default=flag_default("dns_rfc2136"),
+ help="Obtain certificates using a DNS TXT record (if you are using BIND for DNS).")
+ helpful.add(["plugins", "certonly"], "--dns-route53", action="store_true",
+ default=flag_default("dns_route53"),
+ help=("Obtain certificates using a DNS TXT record (if you are using Route53 for "
+ "DNS)."))
+ helpful.add(["plugins", "certonly"], "--dns-sakuracloud", action="store_true",
+ default=flag_default("dns_sakuracloud"),
+ help=("Obtain certificates using a DNS TXT record "
+ "(if you are using Sakura Cloud for DNS)."))
+
+ # 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)
diff --git a/certbot/certbot/_internal/cli/report_config_interaction.py b/certbot/certbot/_internal/cli/report_config_interaction.py
new file mode 100644
index 000000000..39266e776
--- /dev/null
+++ b/certbot/certbot/_internal/cli/report_config_interaction.py
@@ -0,0 +1,27 @@
+"""This is a module that reports config option interaction that should be
+checked by set_by_cli"""
+import six
+
+from certbot._internal.cli import VAR_MODIFIERS
+
+
+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 (string_types)
+ :param modifiers: config options that modify modified
+ :type modifiers: iterable or str (string_types)
+
+ """
+ if isinstance(modified, six.string_types):
+ modified = (modified,)
+ if isinstance(modifiers, six.string_types):
+ modifiers = (modifiers,)
+
+ for var in modified:
+ VAR_MODIFIERS.setdefault(var, set()).update(modifiers)
diff --git a/certbot/certbot/_internal/cli/subparsers.py b/certbot/certbot/_internal/cli/subparsers.py
new file mode 100644
index 000000000..13f8705ce
--- /dev/null
+++ b/certbot/certbot/_internal/cli/subparsers.py
@@ -0,0 +1,72 @@
+"""This module creates subparsers for the argument parser"""
+from certbot import interfaces
+from certbot._internal import constants
+
+from certbot._internal.cli import (
+ flag_default,
+ read_file,
+ CaseInsensitiveList,
+ _user_agent_comment_type,
+ _EncodeReasonAction
+)
+
+
+def _create_subparsers(helpful):
+ from certbot._internal.client import sample_user_agent # avoid import loops
+ helpful.add(
+ None, "--user-agent", default=flag_default("user_agent"),
+ 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, '
+ 'plugin and use case, and to know when to deprecate support for past Python '
+ "versions and flags. If you wish to hide this information from the Let's "
+ 'Encrypt server, set this to "". '
+ '(default: {0}). The flags encoded in the user agent are: '
+ '--duplicate, --force-renew, --allow-subset-of-names, -n, and '
+ 'whether any hooks are set.'.format(sample_user_agent()))
+ helpful.add(
+ None, "--user-agent-comment", default=flag_default("user_agent_comment"),
+ type=_user_agent_comment_type,
+ help="Add a comment to the default user agent string. May be used when repackaging Certbot "
+ "or calling it from another tool to allow additional statistical data to be collected."
+ " Ignored if --user-agent is set. (Example: Foo-Wrapper/1.0)")
+ helpful.add("certonly",
+ "--csr", default=flag_default("csr"), type=read_file,
+ help="Path to a Certificate Signing Request (CSR) in DER or PEM format."
+ " Currently --csr only works with the 'certonly' subcommand.")
+ helpful.add("revoke",
+ "--reason", dest="reason",
+ choices=CaseInsensitiveList(sorted(constants.REVOCATION_REASONS,
+ key=constants.REVOCATION_REASONS.get)),
+ action=_EncodeReasonAction, default=flag_default("reason"),
+ help="Specify reason for revoking certificate. (default: unspecified)")
+ helpful.add("revoke",
+ "--delete-after-revoke", action="store_true",
+ default=flag_default("delete_after_revoke"),
+ help="Delete certificates after revoking them, along with all previous and later "
+ "versions of those certificates.")
+ helpful.add("revoke",
+ "--no-delete-after-revoke", action="store_false",
+ dest="delete_after_revoke",
+ default=flag_default("delete_after_revoke"),
+ help="Do not delete certificates after revoking them. This "
+ "option should be used with caution because the 'renew' "
+ "subcommand will attempt to renew undeleted revoked "
+ "certificates.")
+ 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", default=flag_default("init"),
+ help="Initialize plugins.")
+ helpful.add("plugins",
+ "--prepare", action="store_true", default=flag_default("prepare"),
+ help="Initialize and prepare plugins.")
+ helpful.add("plugins",
+ "--authenticators", action="append_const", dest="ifaces",
+ default=flag_default("ifaces"),
+ const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.")
+ helpful.add("plugins",
+ "--installers", action="append_const", dest="ifaces",
+ default=flag_default("ifaces"),
+ const=interfaces.IInstaller, help="Limit to installer plugins only.")
diff --git a/certbot/certbot/_internal/cli/verb_help.py b/certbot/certbot/_internal/cli/verb_help.py
new file mode 100644
index 000000000..131cfec96
--- /dev/null
+++ b/certbot/certbot/_internal/cli/verb_help.py
@@ -0,0 +1,106 @@
+"""This module contain help information for verbs supported by certbot"""
+from certbot.compat import os
+from certbot._internal.cli import (
+ SHORT_USAGE,
+ flag_default
+)
+
+# The attributes here are:
+# short: a string that will be displayed by "certbot -h commands"
+# opts: a string that heads the section of flags with which this command is documented,
+# both for "certbot -h SUBCOMMAND" and "certbot -h all"
+# usage: an optional string that overrides the header of "certbot -h SUBCOMMAND"
+VERB_HELP = [
+ ("run (default)", {
+ "short": "Obtain/renew a certificate, and install it",
+ "opts": "Options for obtaining & installing certificates",
+ "usage": SHORT_USAGE.replace("[SUBCOMMAND]", ""),
+ "realname": "run"
+ }),
+ ("certonly", {
+ "short": "Obtain or renew a certificate, but do not install it",
+ "opts": "Options for modifying how a certificate is obtained",
+ "usage": ("\n\n certbot certonly [options] [-d DOMAIN] [-d DOMAIN] ...\n\n"
+ "This command obtains a TLS/SSL certificate without installing it anywhere.")
+ }),
+ ("renew", {
+ "short": "Renew all certificates (or one specified with --cert-name)",
+ "opts": ("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"
+ " https://certbot.eff.org/docs/using.html#renewal for more"
+ " information on these."),
+ "usage": "\n\n certbot renew [--cert-name CERTNAME] [options]\n\n"
+ }),
+ ("certificates", {
+ "short": "List certificates managed by Certbot",
+ "opts": "List certificates managed by Certbot",
+ "usage": ("\n\n certbot certificates [options] ...\n\n"
+ "Print information about the status of certificates managed by Certbot.")
+ }),
+ ("delete", {
+ "short": "Clean up all files related to a certificate",
+ "opts": "Options for deleting a certificate",
+ "usage": "\n\n certbot delete --cert-name CERTNAME\n\n"
+ }),
+ ("revoke", {
+ "short": "Revoke a certificate specified with --cert-path or --cert-name",
+ "opts": "Options for revocation of certificates",
+ "usage": "\n\n certbot revoke [--cert-path /path/to/fullchain.pem | "
+ "--cert-name example.com] [options]\n\n"
+ }),
+ ("register", {
+ "short": "Register for account with Let's Encrypt / other ACME server",
+ "opts": "Options for account registration",
+ "usage": "\n\n certbot register --email user@example.com [options]\n\n"
+ }),
+ ("update_account", {
+ "short": "Update existing account with Let's Encrypt / other ACME server",
+ "opts": "Options for account modification",
+ "usage": "\n\n certbot update_account --email updated_email@example.com [options]\n\n"
+ }),
+ ("unregister", {
+ "short": "Irrevocably deactivate your account",
+ "opts": "Options for account deactivation.",
+ "usage": "\n\n certbot unregister [options]\n\n"
+ }),
+ ("install", {
+ "short": "Install an arbitrary certificate in a server",
+ "opts": "Options for modifying how a certificate is deployed",
+ "usage": "\n\n certbot install --cert-path /path/to/fullchain.pem "
+ " --key-path /path/to/private-key [options]\n\n"
+ }),
+ ("rollback", {
+ "short": "Roll back server conf changes made during certificate installation",
+ "opts": "Options for rolling back server configuration changes",
+ "usage": "\n\n certbot rollback --checkpoints 3 [options]\n\n"
+ }),
+ ("plugins", {
+ "short": "List plugins that are installed and available on your system",
+ "opts": 'Options for the "plugins" subcommand',
+ "usage": "\n\n certbot plugins [options]\n\n"
+ }),
+ ("update_symlinks", {
+ "short": "Recreate symlinks in your /etc/letsencrypt/live/ directory",
+ "opts": ("Recreates certificate and key symlinks in {0}, if you changed them by hand "
+ "or edited a renewal configuration file".format(
+ os.path.join(flag_default("config_dir"), "live"))),
+ "usage": "\n\n certbot update_symlinks [options]\n\n"
+ }),
+ ("enhance", {
+ "short": "Add security enhancements to your existing configuration",
+ "opts": ("Helps to harden the TLS configuration by adding security enhancements "
+ "to already existing configuration."),
+ "usage": "\n\n certbot enhance [options]\n\n"
+ }),
+]
+
+
+# VERB_HELP is a list in order to preserve order, but a dict is sometimes useful
+VERB_HELP_MAP = dict(VERB_HELP)
diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py
index 05da1da4e..3a7fb57f8 100644
--- a/certbot/tests/cli_test.py
+++ b/certbot/tests/cli_test.py
@@ -93,7 +93,7 @@ class ParseTest(unittest.TestCase):
return output.getvalue()
- @mock.patch("certbot._internal.cli.flag_default")
+ @mock.patch("certbot._internal.cli.helpful.flag_default")
def test_cli_ini_domains(self, mock_flag_default):
with tempfile.NamedTemporaryFile() as tmp_config:
tmp_config.close() # close now because of compatibility issues on Windows
diff --git a/certbot/tests/helpful_test.py b/certbot/tests/helpful_test.py
new file mode 100644
index 000000000..292e55304
--- /dev/null
+++ b/certbot/tests/helpful_test.py
@@ -0,0 +1,193 @@
+"""Tests for certbot.helpful_parser"""
+import unittest
+
+from certbot import errors
+from certbot._internal.cli import HelpfulArgumentParser
+from certbot._internal.cli import _DomainsAction
+from certbot._internal import constants
+
+
+class TestScanningFlags(unittest.TestCase):
+ '''Test the prescan_for_flag method of HelpfulArgumentParser'''
+ def test_prescan_no_help_flag(self):
+ arg_parser = HelpfulArgumentParser(['run'], {})
+ detected_flag = arg_parser.prescan_for_flag('--help',
+ ['all', 'certonly'])
+ self.assertFalse(detected_flag)
+ detected_flag = arg_parser.prescan_for_flag('-h',
+ ['all, certonly'])
+ self.assertFalse(detected_flag)
+
+ def test_prescan_unvalid_topic(self):
+ arg_parser = HelpfulArgumentParser(['--help', 'all'], {})
+ detected_flag = arg_parser.prescan_for_flag('--help',
+ ['potato'])
+ self.assertIs(detected_flag, True)
+ detected_flag = arg_parser.prescan_for_flag('-h',
+ arg_parser.help_topics)
+ self.assertFalse(detected_flag)
+
+ def test_prescan_valid_topic(self):
+ arg_parser = HelpfulArgumentParser(['-h', 'all'], {})
+ detected_flag = arg_parser.prescan_for_flag('-h',
+ arg_parser.help_topics)
+ self.assertEqual(detected_flag, 'all')
+ detected_flag = arg_parser.prescan_for_flag('--help',
+ arg_parser.help_topics)
+ self.assertFalse(detected_flag)
+
+class TestDetermineVerbs(unittest.TestCase):
+ '''Tests for determine_verb methods of HelpfulArgumentParser'''
+ def test_determine_verb_wrong_verb(self):
+ arg_parser = HelpfulArgumentParser(['potato'], {})
+ self.assertEqual(arg_parser.verb, "run")
+ self.assertEqual(arg_parser.args, ["potato"])
+
+ def test_determine_verb_help(self):
+ arg_parser = HelpfulArgumentParser(['--help', 'everything'], {})
+ self.assertEqual(arg_parser.verb, "help")
+ self.assertEqual(arg_parser.args, ["--help", "everything"])
+ arg_parser = HelpfulArgumentParser(['-d', 'some_domain', '--help',
+ 'all'], {})
+ self.assertEqual(arg_parser.verb, "help")
+ self.assertEqual(arg_parser.args, ['-d', 'some_domain', '--help',
+ 'all'])
+
+ def test_determine_verb(self):
+ arg_parser = HelpfulArgumentParser(['certonly'], {})
+ self.assertEqual(arg_parser.verb, 'certonly')
+ self.assertEqual(arg_parser.args, [])
+
+ arg_parser = HelpfulArgumentParser(['auth'], {})
+ self.assertEqual(arg_parser.verb, 'certonly')
+ self.assertEqual(arg_parser.args, [])
+
+ arg_parser = HelpfulArgumentParser(['everything'], {})
+ self.assertEqual(arg_parser.verb, 'run')
+ self.assertEqual(arg_parser.args, [])
+
+
+class TestAdd(unittest.TestCase):
+ '''Tests for add method in HelpfulArgumentParser'''
+ def test_add_trivial_argument(self):
+ arg_parser = HelpfulArgumentParser(['run'], {})
+ arg_parser.add(None, "--hello-world")
+ parsed_args = arg_parser.parser.parse_args(['--hello-world',
+ 'Hello World!'])
+ self.assertIs(parsed_args.hello_world, 'Hello World!')
+ self.assertFalse(hasattr(parsed_args, 'potato'))
+
+ def test_add_expected_argument(self):
+ arg_parser = HelpfulArgumentParser(['--help', 'run'], {})
+ arg_parser.add(
+ [None, "run", "certonly", "register"],
+ "--eab-kid", dest="eab_kid", action="store",
+ metavar="EAB_KID",
+ help="Key Identifier for External Account Binding")
+ parsed_args = arg_parser.parser.parse_args(["--eab-kid", None])
+ self.assertIs(parsed_args.eab_kid, None)
+ self.assertTrue(hasattr(parsed_args, 'eab_kid'))
+
+
+class TestAddGroup(unittest.TestCase):
+ '''Test add_group method of HelpfulArgumentParser'''
+ def test_add_group_no_input(self):
+ arg_parser = HelpfulArgumentParser(['run'], {})
+ self.assertRaises(TypeError, arg_parser.add_group)
+
+ def test_add_group_topic_not_visible(self):
+ # The user request help on run. A topic that given somewhere in the
+ # args won't be added to the groups in the parser.
+ arg_parser = HelpfulArgumentParser(['--help', 'run'], {})
+ arg_parser.add_group("auth",
+ description="description of auth")
+ self.assertEqual(arg_parser.groups, {})
+
+ def test_add_group_topic_requested_help(self):
+ arg_parser = HelpfulArgumentParser(['--help', 'run'], {})
+ arg_parser.add_group("run",
+ description="description of run")
+ self.assertTrue(arg_parser.groups["run"])
+ arg_parser.add_group("certonly", description="description of certonly")
+ with self.assertRaises(KeyError):
+ self.assertFalse(arg_parser.groups["certonly"])
+
+
+class TestParseArgsErrors(unittest.TestCase):
+ '''Tests for errors that should be met for some cases in parse_args method
+ in HelpfulArgumentParser'''
+ def test_parse_args_renew_force_interactive(self):
+ arg_parser = HelpfulArgumentParser(['renew', '--force-interactive'],
+ {})
+ arg_parser.add(
+ None, constants.FORCE_INTERACTIVE_FLAG, action="store_true")
+
+ with self.assertRaises(errors.Error):
+ arg_parser.parse_args()
+
+ def test_parse_args_non_interactive_and_force_interactive(self):
+ arg_parser = HelpfulArgumentParser(['--force-interactive',
+ '--non-interactive'], {})
+ arg_parser.add(
+ None, constants.FORCE_INTERACTIVE_FLAG, action="store_true")
+ arg_parser.add(
+ None, "--non-interactive", dest="noninteractive_mode",
+ action="store_true"
+ )
+
+ with self.assertRaises(errors.Error):
+ arg_parser.parse_args()
+
+ def test_parse_args_subset_names_wildcard_domain(self):
+ arg_parser = HelpfulArgumentParser(['--domain',
+ '*.example.com,potato.example.com',
+ '--allow-subset-of-names'], {})
+ # The following arguments are added because they have to be defined
+ # in order for arg_parser to run completely. They are not used for the
+ # test.
+ arg_parser.add(
+ None, constants.FORCE_INTERACTIVE_FLAG, action="store_true")
+ arg_parser.add(
+ None, "--non-interactive", dest="noninteractive_mode",
+ action="store_true")
+ arg_parser.add(
+ None, "--staging"
+ )
+ arg_parser.add(None, "--dry-run")
+ arg_parser.add(None, "--csr")
+ arg_parser.add(None, "--must-staple")
+ arg_parser.add(None, "--validate-hooks")
+
+ arg_parser.add(None, "-d", "--domain", dest="domains",
+ metavar="DOMAIN", action=_DomainsAction)
+ arg_parser.add(None, "--allow-subset-of-names")
+ # with self.assertRaises(errors.Error):
+ # arg_parser.parse_args()
+
+ def test_parse_args_hosts_and_auto_hosts(self):
+ arg_parser = HelpfulArgumentParser(['--hsts', '--auto-hsts'], {})
+
+ arg_parser.add(
+ None, "--hsts", action="store_true", dest="hsts")
+ arg_parser.add(
+ None, "--auto-hsts", action="store_true", dest="auto_hsts")
+ # The following arguments are added because they have to be defined
+ # in order for arg_parser to run completely. They are not used for the
+ # test.
+ arg_parser.add(
+ None, constants.FORCE_INTERACTIVE_FLAG, action="store_true")
+ arg_parser.add(
+ None, "--non-interactive", dest="noninteractive_mode",
+ action="store_true")
+ arg_parser.add(None, "--staging")
+ arg_parser.add(None, "--dry-run")
+ arg_parser.add(None, "--csr")
+ arg_parser.add(None, "--must-staple")
+ arg_parser.add(None, "--validate-hooks")
+ arg_parser.add(None, "--allow-subset-of-names")
+ with self.assertRaises(errors.Error):
+ arg_parser.parse_args()
+
+
+if __name__ == '__main__':
+ unittest.main() # pragma: no cover