Compare commits
11 Commits
test-use-p
...
refactor-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7b3d84cad | ||
|
|
a6d4205c02 | ||
|
|
d198017cb5 | ||
|
|
e5b5ae5b81 | ||
|
|
edb557b778 | ||
|
|
bafe734922 | ||
|
|
485348fd8b | ||
|
|
76d2d45f8d | ||
|
|
ffb46d00e9 | ||
|
|
ac20fd1e7e | ||
|
|
dcf30524b7 |
@@ -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
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
524
certbot/certbot/_internal/cli/__init__.py
Normal file
524
certbot/certbot/_internal/cli/__init__.py
Normal file
@@ -0,0 +1,524 @@
|
||||
"""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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
106
certbot/certbot/_internal/cli/cli_constants.py
Normal file
106
certbot/certbot/_internal/cli/cli_constants.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""Certbot command line constants"""
|
||||
import sys
|
||||
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",))}
|
||||
242
certbot/certbot/_internal/cli/cli_utils.py
Normal file
242
certbot/certbot/_internal/cli/cli_utils.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""Certbot command line util function"""
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
|
||||
from acme import challenges
|
||||
from certbot import interfaces
|
||||
from certbot import util
|
||||
from certbot import errors
|
||||
from certbot._internal import constants
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
import zope.interface.interface
|
||||
from acme.magic_typing import Any, Dict, Optional
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
|
||||
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
|
||||
19
certbot/certbot/_internal/cli/group_adder.py
Normal file
19
certbot/certbot/_internal/cli/group_adder.py
Normal file
@@ -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"])
|
||||
468
certbot/certbot/_internal/cli/helpful.py
Normal file
468
certbot/certbot/_internal/cli/helpful.py
Normal file
@@ -0,0 +1,468 @@
|
||||
"""Certbot command line argument parser"""
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
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._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}
|
||||
51
certbot/certbot/_internal/cli/paths_parser.py
Normal file
51
certbot/certbot/_internal/cli/paths_parser.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""This is a module that adds configuration to the argument parser regarding
|
||||
paths for certificates"""
|
||||
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"))
|
||||
97
certbot/certbot/_internal/cli/plugins_parsing.py
Normal file
97
certbot/certbot/_internal/cli/plugins_parsing.py
Normal file
@@ -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)
|
||||
27
certbot/certbot/_internal/cli/report_config_interaction.py
Normal file
27
certbot/certbot/_internal/cli/report_config_interaction.py
Normal file
@@ -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)
|
||||
72
certbot/certbot/_internal/cli/subparsers.py
Normal file
72
certbot/certbot/_internal/cli/subparsers.py
Normal file
@@ -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.")
|
||||
107
certbot/certbot/_internal/cli/verb_help.py
Normal file
107
certbot/certbot/_internal/cli/verb_help.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""This module contain help information for verbs supported by certbot"""
|
||||
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)
|
||||
@@ -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
|
||||
|
||||
193
certbot/tests/helpful_test.py
Normal file
193
certbot/tests/helpful_test.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user