certbot/letsencrypt/cli.py

970 lines
42 KiB
Python
Raw Normal View History

2016-03-14 21:44:29 -04:00
"""Let's Encrypt command line argument & config processing."""
from __future__ import print_function
import argparse
import glob
2015-11-21 13:59:05 -05:00
import json
import logging
2015-06-22 08:41:08 -04:00
import logging.handlers
2015-04-22 03:22:15 -04:00
import os
import sys
2015-06-29 20:31:48 -04:00
import traceback
import configargparse
2015-09-08 04:33:03 -04:00
import OpenSSL
import six
2014-12-17 06:02:15 -05:00
import letsencrypt
2015-05-10 08:25:29 -04:00
from letsencrypt import constants
2015-09-09 16:04:28 -04:00
from letsencrypt import crypto_util
2015-11-08 14:04:48 -05:00
from letsencrypt import errors
2016-03-29 21:45:14 -04:00
from letsencrypt import hooks
2015-05-10 08:25:29 -04:00
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt.plugins import disco as plugins_disco
2016-03-11 19:01:52 -05:00
import letsencrypt.plugins.selection as plugin_selection
2016-02-29 02:34:44 -05:00
logger = logging.getLogger(__name__)
2016-02-05 21:25:38 -05:00
# Global, to save us from a lot of argument passing within the scope of this module
2016-03-11 15:29:31 -05:00
helpful_parser = None
# For help strings, figure out how the user ran us.
# When invoked from letsencrypt-auto, sys.argv[0] is something like:
# "/home/user/.local/share/letsencrypt/bin/letsencrypt"
# Note that this won't work if the user set VENV_PATH or XDG_DATA_HOME before
# running letsencrypt-auto (and sudo stops us from seeing if they did), so it
# should only be used for purposes where inability to detect letsencrypt-auto
# fails safely
fragment = os.path.join(".local", "share", "letsencrypt")
cli_command = "letsencrypt-auto" if fragment in sys.argv[0] else "letsencrypt"
# 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] ...
2015-06-22 12:37:57 -04:00
The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates. By
default, it will attempt to use a webserver both for obtaining and installing
2015-10-23 19:49:38 -04:00
the cert. Major SUBCOMMANDS are:
(default) run Obtain & install a cert in your current webserver
2015-10-26 14:27:31 -04:00
certonly Obtain cert, but do not install it (aka "auth")
install Install a previously obtained cert in a server
renew Renew previously obtained certs that are near expiry
revoke Revoke a previously obtained certificate
rollback Rollback server configuration changes made during install
config_changes Show changes made to server config during installation
2015-12-01 18:19:25 -05:00
plugins Display information about installed plugins
2015-06-22 12:37:57 -04:00
""".format(cli_command)
2015-10-23 19:49:38 -04:00
# This is the short help for letsencrypt --help, where we disable argparse
# altogether
2015-10-26 14:27:31 -04:00
USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert:
2015-06-22 12:37:57 -04:00
%s
--standalone Run a standalone webserver for authentication
%s
2015-11-17 19:29:06 -05:00
--webroot Place files in a server's webroot folder for authentication
2015-10-26 14:27:31 -04:00
2015-12-01 18:19:25 -05:00
OR use different plugins to obtain (authenticate) the cert and then install it:
2015-10-26 14:27:31 -04:00
--authenticator standalone --installer apache
2015-06-22 12:37:57 -04:00
More detailed help:
2015-09-08 04:33:03 -04:00
-h, --help [topic] print this message, or detailed help on a topic;
2015-06-22 12:37:57 -04:00
the available topics are:
2015-10-26 14:27:31 -04:00
all, automation, paths, security, testing, or any of the subcommands or
2015-11-17 19:29:06 -05:00
plugins (certonly, install, nginx, apache, standalone, webroot, etc)
"""
2015-10-30 16:54:46 -04:00
2016-03-31 23:25:25 -04:00
# These argparse parameters should be removed when detecting defaults.
ARGPARSE_PARAMS_TO_REMOVE = ("const", "nargs", "type",)
2016-03-31 21:08:36 -04:00
2016-03-31 23:25:25 -04:00
# These sets are used when to help detect options set by the user.
2016-03-31 21:08:36 -04:00
EXIT_ACTIONS = set(("help", "version",))
ZERO_ARG_ACTIONS = set(("store_const", "store_true",
"store_false", "append_const", "count",))
2016-03-31 23:19:14 -04:00
# 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.
2016-03-31 23:19:14 -04:00
VAR_MODIFIERS = {"account": set(("server",)),
"server": set(("dry_run", "staging",)),
"webroot_map": set(("webroot_path",))}
2016-03-31 23:07:18 -04:00
def report_config_interaction(modified, modifiers):
"""Registers config option interaction to be checked by set_by_cli.
2016-04-01 00:51:15 -04:00
This function can be called by during the __init__ or
add_parser_arguments methods of plugins to register interactions
between config options.
2016-03-31 23:07:18 -04:00
:param modified: config options that can be modified by modifiers
:type modified: iterable or str
:param modifiers: config options that modify modified
:type modifiers: iterable or str
"""
if isinstance(modified, str):
2016-03-31 23:19:14 -04:00
modified = (modified,)
2016-03-31 23:07:18 -04:00
if isinstance(modifiers, str):
2016-03-31 23:19:14 -04:00
modifiers = (modifiers,)
2016-03-31 23:07:18 -04:00
for var in modified:
2016-03-31 23:19:14 -04:00
VAR_MODIFIERS.setdefault(var, set()).update(modifiers)
2016-03-31 23:07:18 -04:00
def usage_strings(plugins):
"""Make usage strings late so that plugins can be initialised late"""
if "nginx" in plugins:
nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
else:
nginx_doc = "(nginx support is experimental, buggy, and not installed by default)"
if "apache" in plugins:
apache_doc = "--apache Use the Apache plugin for authentication & installation"
else:
apache_doc = "(the apache plugin is not installed)"
return USAGE % (apache_doc, nginx_doc), SHORT_USAGE
2015-06-22 12:37:57 -04:00
2016-03-31 18:31:26 -04:00
class _Default(object):
2016-03-31 21:08:36 -04:00
"""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__()
2016-03-31 18:31:26 -04:00
2016-03-11 15:29:31 -05:00
def set_by_cli(var):
2016-02-05 21:25:38 -05:00
"""
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.
2016-02-05 21:25:38 -05:00
"""
2016-03-11 15:29:31 -05:00
detector = set_by_cli.detector
2016-02-09 13:41:52 -05:00
if detector is None:
2016-02-05 21:25:38 -05:00
# 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()
2016-02-07 23:08:32 -05:00
# reconstructed_args == sys.argv[1:], or whatever was passed to main()
2016-03-11 15:29:31 -05:00
reconstructed_args = helpful_parser.args + [helpful_parser.verb]
detector = set_by_cli.detector = prepare_and_parse_args(
plugins, reconstructed_args, detect_defaults=True)
# propagate plugin requests: eg --standalone modifies config.authenticator
2016-03-31 21:53:31 -04:00
detector.authenticator, detector.installer = (
plugin_selection.cli_plugin_requests(detector))
2016-02-09 21:34:44 -05:00
logger.debug("Default Detector is %r", detector)
if not isinstance(getattr(detector, var), _Default):
return True
for modifier in VAR_MODIFIERS.get(var, []):
if set_by_cli(modifier):
return True
return False
# static housekeeping var
2016-03-11 15:29:31 -05:00
set_by_cli.detector = None
def argparse_type(variable):
"Return our argparse type function for a config variable (default: str)"
# pylint: disable=protected-access
for action in helpful_parser.parser._actions:
if action.type is not None and action.dest == variable:
return action.type
return str
def read_file(filename, mode="rb"):
"""Returns the given file's contents.
2015-03-30 08:34:22 -04:00
2015-11-13 12:00:54 -05:00
:param str filename: path to file
:param str mode: open mode (see `open`)
2015-03-30 08:34:22 -04:00
2015-11-13 12:00:54 -05:00
:returns: absolute path of filename and its contents
2015-03-30 08:34:22 -04:00
:rtype: tuple
:raises argparse.ArgumentTypeError: File does not exist or is not readable.
"""
try:
2015-11-12 20:19:26 -05:00
filename = os.path.abspath(filename)
return filename, open(filename, mode).read()
2015-03-30 08:34:22 -04:00
except IOError as exc:
raise argparse.ArgumentTypeError(exc.strerror)
2015-03-26 18:00:00 -04:00
def flag_default(name):
"""Default value for CLI flag."""
# XXX: this is an internal housekeeping notion of defaults before
# argparse has been set up; it is not accurate for all flags. Call it
# with caution. Plugin defaults are missing, and some things are using
# defaults defined in this file, not in constants.py :(
return constants.CLI_DEFAULTS[name]
2015-06-24 17:38:30 -04:00
2015-06-06 18:45:17 -04:00
def config_help(name, hidden=False):
"""Extract the help message for an `.IConfig` attribute."""
2015-06-06 18:45:17 -04:00
if hidden:
return argparse.SUPPRESS
else:
return interfaces.IConfig[name].__doc__
2015-03-26 18:00:00 -04:00
2015-06-24 17:38:30 -04:00
2016-03-31 21:12:11 -04:00
class HelpfulArgumentGroup(object):
"""Emulates an argparse group for use with HelpfulArgumentParser.
2015-03-26 18:00:00 -04:00
2016-03-31 21:12:11 -04:00
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.
2015-06-24 17:38:30 -04:00
"""
2016-03-31 21:12:11 -04:00
def __init__(self, helpful_arg_parser, topic):
self._parser = helpful_arg_parser
self._topic = topic
2015-09-06 05:20:11 -04:00
def add_argument(self, *args, **kwargs):
2016-03-31 21:12:11 -04:00
"""Add a new command line argument to the argument group."""
self._parser.add(self._topic, *args, **kwargs)
2015-06-16 15:46:37 -04:00
class HelpfulArgumentParser(object):
2015-06-24 17:38:30 -04:00
"""Argparse Wrapper.
This class wraps argparse, adding the ability to make --help less
verbose, and request help on specific subcategories at a time, eg
2015-06-24 17:38:30 -04:00
'letsencrypt --help security' for security options.
2015-06-24 17:38:30 -04:00
"""
2015-10-23 19:49:38 -04:00
def __init__(self, args, plugins, detect_defaults=False):
from letsencrypt import main
2016-03-14 23:11:31 -04:00
self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert,
"config_changes": main.config_changes, "run": main.run,
"install": main.install, "plugins": main.plugins_cmd,
"renew": main.renew, "revoke": main.revoke,
2016-03-14 23:11:31 -04:00
"rollback": main.rollback, "everything": main.run}
# List of topics for which additional help can be provided
2016-03-14 21:44:29 -04:00
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
2015-10-23 19:49:38 -04:00
2016-03-14 21:44:29 -04:00
plugin_names = list(plugins)
self.help_topics = HELP_TOPICS + plugin_names + [None]
usage, short_usage = usage_strings(plugins)
self.parser = configargparse.ArgParser(
usage=short_usage,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
args_for_setting_config_path=["-c", "--config"],
default_config_files=flag_default("config_files"))
2015-06-23 18:10:42 -04:00
# This is the only way to turn off overly verbose config flag documentation
2015-09-06 05:20:11 -04:00
self.parser._add_config_file_help = False # pylint: disable=protected-access
self.detect_defaults = detect_defaults
2015-10-23 19:49:38 -04:00
self.args = args
self.determine_verb()
2015-06-23 18:10:42 -04:00
help1 = self.prescan_for_flag("-h", self.help_topics)
help2 = self.prescan_for_flag("--help", self.help_topics)
2015-06-16 15:46:37 -04:00
assert max(True, "a") == "a", "Gravity changed direction"
self.help_arg = max(help1, help2)
if self.help_arg is True:
2015-06-23 18:10:42 -04:00
# just --help with no topic; avoid argparse altogether
print(usage)
2015-06-23 18:10:42 -04:00
sys.exit(0)
self.visible_topics = self.determine_help_topics(self.help_arg)
self.groups = {} # elements are added by .add_group()
2015-10-23 19:49:38 -04:00
def parse_args(self):
"""Parses command line arguments and returns the result.
:returns: parsed command line arguments
:rtype: argparse.Namespace
2015-08-28 20:22:58 -04:00
"""
2015-10-23 19:49:38 -04:00
parsed_args = self.parser.parse_args(self.args)
parsed_args.func = self.VERBS[self.verb]
parsed_args.verb = self.verb
2015-10-23 19:49:38 -04:00
2016-03-31 21:08:36 -04:00
if self.detect_defaults:
return parsed_args
# Do any post-parsing homework here
# we get domains from -d, but also from the webroot map...
if parsed_args.webroot_map:
for domain in parsed_args.webroot_map.keys():
if domain not in parsed_args.domains:
parsed_args.domains.append(domain)
2016-01-28 21:00:28 -05:00
if parsed_args.staging or parsed_args.dry_run:
if parsed_args.server not in (flag_default("server"), constants.STAGING_URI):
2016-01-28 21:00:28 -05:00
conflicts = ["--staging"] if parsed_args.staging else []
conflicts += ["--dry-run"] if parsed_args.dry_run else []
if not self.detect_defaults:
raise errors.Error("--server value conflicts with {0}".format(
" and ".join(conflicts)))
2015-12-10 22:37:22 -05:00
parsed_args.server = constants.STAGING_URI
2016-01-28 21:00:28 -05:00
if parsed_args.dry_run:
if self.verb not in ["certonly", "renew"]:
2016-01-28 21:00:28 -05:00
raise errors.Error("--dry-run currently only works with the "
"'certonly' or 'renew' subcommands (%r)" % self.verb)
2016-01-28 21:24:47 -05:00
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.agree_tos = True
parsed_args.register_unsafely_without_email = True
2015-10-23 19:49:38 -04:00
if parsed_args.csr:
2016-03-20 19:12:59 -04:00
if parsed_args.allow_subset_of_names:
raise errors.Error("--allow-subset-of-names "
"cannot be used with --csr")
self.handle_csr(parsed_args)
2016-03-29 21:45:14 -04:00
hooks.validate_hooks(parsed_args)
2015-10-23 19:49:38 -04:00
return parsed_args
def handle_csr(self, parsed_args):
"""
Process a --csr flag. This needs to happen early enough that the
2016-02-29 02:34:44 -05:00
webroot plugin can know about the calls to process_domain
"""
if parsed_args.verb != "certonly":
raise errors.Error("Currently, a CSR file may only be specified "
"when obtaining a new or replacement "
"via the certonly command. Please try the "
"certonly command instead.")
try:
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der")
typ = OpenSSL.crypto.FILETYPE_ASN1
domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1)
except OpenSSL.crypto.Error:
try:
e1 = traceback.format_exc()
typ = OpenSSL.crypto.FILETYPE_PEM
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem")
domains = crypto_util.get_sans_from_csr(csr.data, typ)
except OpenSSL.crypto.Error:
logger.debug("DER CSR parse error %s", e1)
logger.debug("PEM CSR parse error %s", traceback.format_exc())
raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0]))
2016-02-09 00:10:34 -05:00
for d in domains:
2016-02-29 02:34:44 -05:00
process_domain(parsed_args, d)
2016-02-09 00:10:34 -05:00
for d in domains:
2016-02-09 00:14:45 -05:00
sanitised = le_util.enforce_domain_sanity(d)
2016-02-09 00:10:34 -05:00
if d.lower() != sanitised:
raise errors.ConfigurationError(
"CSR domain {0} needs to be sanitised to {1}.".format(d, sanitised))
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])
2016-02-09 00:10:34 -05:00
parsed_args.actual_csr = (csr, typ)
csr_domains, config_domains = set(domains), set(parsed_args.domains)
if csr_domains != config_domains:
raise errors.ConfigurationError(
"Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}"
2016-02-09 00:10:34 -05:00
.format(", ".join(csr_domains), ", ".join(config_domains)))
2015-10-23 19:49:38 -04:00
def determine_verb(self):
2015-10-23 21:26:33 -04:00
"""Determines the verb/subcommand provided by the user.
This function works around some of the limitations of argparse.
"""
2015-10-23 19:49:38 -04:00
if "-h" in self.args or "--help" in self.args:
# all verbs double as help arguments; don't get them confused
self.verb = "help"
2015-10-23 19:49:38 -04:00
return
2015-10-23 19:49:38 -04:00
for i, token in enumerate(self.args):
if token in self.VERBS:
2015-10-26 13:56:32 -04:00
verb = token
if verb == "auth":
verb = "certonly"
2015-10-26 13:56:32 -04:00
if verb == "everything":
verb = "run"
self.verb = verb
2015-10-23 19:49:38 -04:00
self.args.pop(i)
return
self.verb = "run"
2015-08-28 20:22:58 -04:00
def prescan_for_flag(self, flag, possible_arguments):
2015-06-24 17:38:30 -04:00
"""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
2015-06-24 17:38:30 -04:00
present.
"""
if flag not in self.args:
return False
pos = self.args.index(flag)
2015-06-16 15:46:37 -04:00
try:
nxt = self.args[pos + 1]
if nxt in possible_arguments:
return nxt
2015-06-16 15:46:37 -04:00
except IndexError:
pass
return True
def add(self, topic, *args, **kwargs):
2015-06-24 17:38:30 -04:00
"""Add a new command line argument.
:param str: help topic this should be listed under, can be None for
"always documented"
:param list *args: the names of this argument flag
:param dict **kwargs: various argparse settings for this argument
2015-06-16 15:46:37 -04:00
2015-06-24 17:38:30 -04:00
"""
if self.detect_defaults:
2016-03-31 21:08:36 -04:00
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)
2016-03-31 21:08:36 -04:00
def modify_kwargs_for_default_detection(self, **kwargs):
"""Modify an arg so we can check if it was set by the user.
2016-03-31 21:08:36 -04:00
Changes the parameters given to argparse when adding an argument
so we can properly detect if the value was set by the user.
2016-03-31 21:08:36 -04:00
:param dict kwargs: various argparse settings for this argument
:returns: a modified versions of kwargs
2016-03-31 21:08:36 -04:00
:rtype: dict
"""
2016-03-31 21:08:36 -04:00
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
2015-12-01 19:33:15 -05:00
def add_deprecated_argument(self, argument_name, num_args):
"""Adds a deprecated argument with the name argument_name.
Deprecated arguments are not shown in the help. If they are used
on the command line, a warning is shown stating that the
argument is deprecated and no other action is taken.
:param str argument_name: Name of deprecated argument.
:param int nargs: Number of arguments the option takes.
"""
le_util.add_deprecated_argument(
self.parser.add_argument, argument_name, num_args)
def add_group(self, topic, **kwargs):
2016-03-31 21:12:11 -04:00
"""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.
2015-06-24 17:38:30 -04:00
2016-03-31 21:12:11 -04:00
:param str topic: Name of the new argument group.
2015-06-24 17:38:30 -04:00
2016-03-31 21:12:11 -04:00
:returns: The new argument group.
:rtype: `HelpfulArgumentGroup`
2015-06-24 17:38:30 -04:00
"""
if self.visible_topics[topic]:
2016-03-31 21:12:11 -04:00
self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)
return HelpfulArgumentGroup(self, topic)
def add_plugin_args(self, plugins):
2015-06-24 17:38:30 -04:00
"""
Let each of the plugins add its own command line arguments, which
may or may not be displayed as help topics.
"""
for name, plugin_ep in six.iteritems(plugins):
parser_or_group = self.add_group(name, description=plugin_ep.description)
plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name)
def determine_help_topics(self, chosen_topic):
2015-06-24 17:38:30 -04:00
"""
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
2015-06-24 17:38:30 -04:00
"""
# topics maps each topic to whether it should be documented by
# argparse on the command line
if chosen_topic == "auth":
chosen_topic = "certonly"
2015-10-26 14:27:31 -04:00
if chosen_topic == "everything":
chosen_topic = "run"
if chosen_topic == "all":
2015-06-16 15:46:37 -04:00
return dict([(t, True) for t in self.help_topics])
elif not chosen_topic:
2015-06-16 15:46:37 -04:00
return dict([(t, False) for t in self.help_topics])
else:
2015-06-16 15:46:37 -04:00
return dict([(t, t == chosen_topic) for t in self.help_topics])
def prepare_and_parse_args(plugins, args, detect_defaults=False):
2015-10-23 19:49:38 -04:00
"""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)
2015-03-30 08:34:22 -04:00
# --help is automatically provided by argparse
helpful.add(
None, "-v", "--verbose", dest="verbose_count", action="count",
2015-06-01 17:08:42 -04:00
default=flag_default("verbose_count"), help="This flag can be used "
"multiple times to incrementally increase the verbosity of output, "
"e.g. -vvv.")
helpful.add(
None, "-t", "--text", dest="text_mode", action="store_true",
help="Use the text output instead of the curses UI.")
2015-12-29 03:12:08 -05:00
helpful.add(
None, "-n", "--non-interactive", "--noninteractive",
dest="noninteractive_mode", action="store_true",
help="Run without ever asking for user input. This may require "
"additional command line flags; the client will try to explain "
"which ones are required if it finds one missing")
2016-02-10 13:58:07 -05:00
helpful.add(
None, "--dry-run", action="store_true", dest="dry_run",
help="Perform a test run of the client, obtaining test (invalid) certs"
" but not saving them to disk. This can currently only be used"
" with the 'certonly' and 'renew' subcommands. \nNote: Although --dry-run"
" tries to avoid making any persistent changes on a system, it "
" is not completely side-effect free: if used with webserver authenticator plugins"
" like apache and nginx, it makes and then reverts temporary config changes"
" in order to obtain test certs, and reloads webservers to deploy and then"
" roll back those changes. It also calls --pre-hook and --post-hook commands"
" if they are defined because they may be necessary to accurately simulate"
" renewal. --renew-hook commands are not called.")
helpful.add(
2015-10-25 06:17:25 -04:00
None, "--register-unsafely-without-email", action="store_true",
help="Specifying this flag enables registering an account with no "
"email address. This is strongly discouraged, because in the "
"event of key loss or account compromise you will irrevocably "
"lose access to your account. You will also be unable to receive "
"notice about impending expiration or revocation of your "
"certificates. Updates to the Subscriber Agreement will still "
"affect you, and will be effective 14 days after posting an "
"update to the web site.")
helpful.add(None, "-m", "--email", help=config_help("email"))
# positional arg shadows --domains, instead of appending, and
# --domains is useful, because it can be stored in config
#for subparser in parser_run, parser_auth, parser_install:
# subparser.add_argument("domains", nargs="*", metavar="domain")
helpful.add(None, "-d", "--domains", "--domain", dest="domains",
2015-12-01 19:33:35 -05:00
metavar="DOMAIN", action=DomainFlagProcessor, default=[],
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains "
"as a parameter.")
2015-06-24 17:38:30 -04:00
helpful.add_group(
"automation",
description="Arguments for automating execution & other tweaks")
helpful.add(
2015-12-12 20:01:43 -05:00
"automation", "--keep-until-expiring", "--keep", "--reinstall",
dest="reinstall", action="store_true",
help="If the requested cert matches an existing cert, always keep the "
"existing one until it is due for renewal (for the "
"'run' subcommand this means reinstall the existing cert)")
helpful.add(
"automation", "--expand", action="store_true",
help="If an existing cert covers some subset of the requested names, "
"always expand and replace it with the additional names.")
helpful.add(
2015-06-16 15:46:37 -04:00
"automation", "--version", action="version",
version="%(prog)s {0}".format(letsencrypt.__version__),
help="show program's version number and exit")
helpful.add(
"automation", "--force-renewal", "--renew-by-default",
action="store_true", dest="renew_by_default", help="If a certificate "
"already exists for the requested domains, renew it now, "
"regardless of whether it is near expiry. (Often "
"--keep-until-expiring is more appropriate). Also implies "
"--expand.")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
2015-06-06 18:45:17 -04:00
help="Agree to the Let's Encrypt Subscriber Agreement")
helpful.add(
"automation", "--account", metavar="ACCOUNT_ID",
help="Account ID to use")
helpful.add(
"automation", "--duplicate", dest="duplicate", action="store_true",
help="Allow making a certificate lineage that duplicates an existing one "
2015-12-12 17:14:18 -05:00
"(both can be renewed in parallel)")
helpful.add(
"automation", "--os-packages-only", action="store_true",
help="(letsencrypt-auto only) install OS package dependencies and then stop")
helpful.add(
"automation", "--no-self-upgrade", action="store_true",
help="(letsencrypt-auto only) prevent the letsencrypt-auto script from"
" upgrading itself to newer released versions")
2015-06-11 14:05:00 -04:00
2015-06-16 17:04:41 -04:00
helpful.add_group(
"testing", description="The following flags are meant for "
"testing purposes only! Do NOT change them, unless you "
"really know what you're doing!")
2015-06-26 18:26:33 -04:00
helpful.add(
"testing", "--debug", action="store_true",
help="Show tracebacks in case of errors, and allow letsencrypt-auto "
"execution on experimental platforms")
helpful.add(
2015-06-16 15:46:37 -04:00
"testing", "--no-verify-ssl", action="store_true",
help=config_help("no_verify_ssl"),
default=flag_default("no_verify_ssl"))
2015-10-25 09:05:49 -04:00
helpful.add(
2015-11-07 09:21:58 -05:00
"testing", "--tls-sni-01-port", type=int,
default=flag_default("tls_sni_01_port"),
help=config_help("tls_sni_01_port"))
2015-11-20 19:09:39 -05:00
helpful.add(
"testing", "--http-01-port", type=int, dest="http01_port",
default=flag_default("http01_port"), help=config_help("http01_port"))
helpful.add(
"testing", "--break-my-certs", action="store_true",
help="Be willing to replace or renew valid certs with invalid "
"(testing/staging) certs")
helpful.add_group(
"security", description="Security parameters & server settings")
helpful.add(
2015-10-25 09:04:49 -04:00
"security", "--rsa-key-size", type=int, metavar="N",
default=flag_default("rsa_key_size"), help=config_help("rsa_key_size"))
helpful.add(
2015-10-25 09:04:49 -04:00
"security", "--redirect", action="store_true",
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
2015-10-24 14:57:24 -04:00
"authenticated vhost.", dest="redirect", default=None)
helpful.add(
2015-10-25 09:04:49 -04:00
"security", "--no-redirect", action="store_false",
2015-10-24 14:57:24 -04:00
help="Do not automatically redirect all HTTP traffic to HTTPS for the newly "
"authenticated vhost.", dest="redirect", default=None)
2015-11-10 01:41:59 -05:00
helpful.add(
"security", "--hsts", action="store_true",
help="Add the Strict-Transport-Security header to every HTTP response."
" Forcing browser to use always use SSL for the domain."
2015-11-16 15:41:39 -05:00
" Defends against SSL Stripping.", dest="hsts", default=False)
helpful.add(
"security", "--no-hsts", action="store_false",
help="Do not automatically add the Strict-Transport-Security header"
" to every HTTP response.", dest="hsts", default=False)
2015-11-10 01:41:59 -05:00
helpful.add(
"security", "--uir", action="store_true",
help="Add the \"Content-Security-Policy: upgrade-insecure-requests\""
" header to every HTTP response. Forcing the browser to use"
" https:// for every http:// resource.", dest="uir", default=None)
helpful.add(
"security", "--no-uir", action="store_false",
help=" Do not automatically set the \"Content-Security-Policy:"
" upgrade-insecure-requests\" header to every HTTP response.",
dest="uir", default=None)
helpful.add(
"security", "--strict-permissions", action="store_true",
help="Require that all configuration files are owned by the current "
2015-09-16 16:20:31 -04:00
"user; only needed if your config is somewhere unsafe like /tmp/")
2016-02-01 05:32:08 -05:00
helpful.add(
"automation", "--allow-subset-of-names",
action="store_true",
help="When performing domain validation, do not consider it a failure "
"if authorizations can not be obtained for a strict subset of "
"the requested domains. This option cannot be used with --csr.")
2016-02-10 13:58:07 -05:00
helpful.add_group(
"renew", description="The 'renew' subcommand will attempt to renew all"
" certificates (or more precisely, certificate lineages) you have"
" previously obtained if they are close to expiry, and print a"
" summary of the results. By default, 'renew' will reuse the options"
" used to create obtain or most recently successfully renew each"
" certificate lineage. You can try it with `--dry-run` first. For"
" more fine-grained control, you can renew individual lineages with"
" the `certonly` subcommand. Hooks are available to run commands "
" before and after renewal; see XXX for more information on these.")
helpful.add(
"renew", "--pre-hook",
help="Command to be run in a shell before obtaining any certificates. Intended"
" primarily for renewal, where it can be used to temporarily shut down a"
" webserver that might conflict with the standalone plugin. This will "
" only be called if a certificate is actually to be obtained/renewed. ")
helpful.add(
"renew", "--post-hook",
help="Command to be run in a shell after attempting to obtain/renew "
" certificates. Can be used to deploy renewed certificates, or to restart"
" any servers that were stopped by --pre-hook.")
helpful.add(
"renew", "--renew-hook",
2016-03-30 16:56:10 -04:00
help="Command to be run in a shell once for each successfully renewed certificate."
2016-03-29 21:33:57 -04:00
"For this command, the shell variable $RENEWED_LINEAGE will point to the"
"config live subdirectory containing the new certs and keys; the shell variable "
2016-04-02 04:50:18 -04:00
"$RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains")
2016-02-10 13:58:07 -05:00
2015-12-01 19:51:05 -05:00
helpful.add_deprecated_argument("--agree-dev-preview", 0)
_create_subparsers(helpful)
_paths_parser(helpful)
# _plugins_parsing should be the last thing to act upon the main
# parser (--help should display plugin-specific options last)
_plugins_parsing(helpful, plugins)
2016-02-08 19:59:11 -05:00
if not detect_defaults:
2016-03-11 15:29:31 -05:00
global helpful_parser # pylint: disable=global-statement
helpful_parser = helpful
2015-10-23 19:49:38 -04:00
return helpful.parse_args()
def _create_subparsers(helpful):
helpful.add_group("certonly", description="Options for modifying how a cert is obtained")
helpful.add_group("install", description="Options for modifying how a cert is deployed")
helpful.add_group("revoke", description="Options for revocation of certs")
helpful.add_group("rollback", description="Options for reverting config changes")
helpful.add_group("plugins", description="Plugin options")
2016-02-18 17:49:57 -05:00
helpful.add_group("config_changes",
description="Options for showing a history of config changes")
helpful.add("config_changes", "--num", type=int,
help="How many past revisions you want to be displayed")
2015-11-09 20:42:46 -05:00
helpful.add(
2015-11-13 15:48:38 -05:00
None, "--user-agent", default=None,
2015-11-09 20:42:46 -05:00
help="Set a custom user agent string for the client. User agent strings allow "
"the CA to collect high level statistics about success rates by OS and "
"plugin. If you wish to hide your server OS version from the Let's "
2015-12-01 00:08:23 -05:00
'Encrypt server, set this to "".')
helpful.add("certonly",
"--csr", type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER"
" format; note that the .csr file *must* contain a Subject"
" Alternative Name field for each domain you want certified."
" Currently --csr only works with the 'certonly' subcommand'")
helpful.add("rollback",
"--checkpoints", type=int, metavar="N",
default=flag_default("rollback_checkpoints"),
help="Revert configuration N number of checkpoints.")
helpful.add("plugins",
"--init", action="store_true", help="Initialize plugins.")
helpful.add("plugins",
"--prepare", action="store_true", help="Initialize and prepare plugins.")
helpful.add("plugins",
"--authenticators", action="append_const", dest="ifaces",
const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.")
helpful.add("plugins",
"--installers", action="append_const", dest="ifaces",
const=interfaces.IInstaller, help="Limit to installer plugins only.")
2015-04-22 05:16:13 -04:00
2015-02-16 02:17:53 -05:00
def _paths_parser(helpful):
add = helpful.add
verb = helpful.verb
if verb == "help":
2015-10-30 17:56:08 -04:00
verb = helpful.help_arg
helpful.add_group(
"paths", description="Arguments changing execution paths & servers")
cph = "Path to where cert is saved (with auth --csr), installed from or revoked."
section = "paths"
if verb in ("install", "revoke", "certonly"):
2015-10-30 17:56:08 -04:00
section = verb
if verb == "certonly":
add(section, "--cert-path", type=os.path.abspath,
default=flag_default("auth_cert_path"), help=cph)
elif verb == "revoke":
add(section, "--cert-path", type=read_file, required=True, help=cph)
else:
add(section, "--cert-path", type=os.path.abspath,
help=cph, required=(verb == "install"))
section = "paths"
if verb in ("install", "revoke"):
2015-10-30 17:56:08 -04:00
section = verb
# revoke --key-path reads a file, install --key-path takes a string
add(section, "--key-path", required=(verb == "install"),
type=((verb == "revoke" and read_file) or os.path.abspath),
2015-11-13 17:02:34 -05:00
help="Path to private key for cert installation "
"or revocation (if account key is missing)")
default_cp = None
if verb == "certonly":
default_cp = flag_default("auth_chain_path")
add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath,
help="Accompanying path to a full certificate chain (cert plus chain).")
add("paths", "--chain-path", default=default_cp, type=os.path.abspath,
help="Accompanying path to a certificate chain.")
add("paths", "--config-dir", default=flag_default("config_dir"),
help=config_help("config_dir"))
add("paths", "--work-dir", default=flag_default("work_dir"),
help=config_help("work_dir"))
2015-06-22 08:41:08 -04:00
add("paths", "--logs-dir", default=flag_default("logs_dir"),
help="Logs directory.")
add("paths", "--server", default=flag_default("server"),
help=config_help("server"))
# overwrites server, handled in HelpfulArgumentParser.parse_args()
add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
help='Use the staging server to obtain test (invalid) certs; equivalent'
2015-12-10 22:37:22 -05:00
' to --server ' + constants.STAGING_URI)
2015-04-22 03:22:15 -04:00
def _plugins_parsing(helpful, plugins):
helpful.add_group(
"plugins", description="Let's Encrypt client supports an "
"extensible plugins architecture. See '%(prog)s plugins' for a "
2015-12-01 18:19:25 -05:00
"list of all installed plugins and their names. You can force "
"a particular plugin by setting options provided below. Running "
"--help <plugin_name> will list flags specific to that plugin.")
helpful.add(
"plugins", "-a", "--authenticator", help="Authenticator plugin name.")
helpful.add(
"plugins", "-i", "--installer", help="Installer plugin name (also used to find domains).")
helpful.add(
"plugins", "--configurator", help="Name of the plugin that is "
"both an authenticator and an installer. Should not be used "
"together with --authenticator or --installer.")
helpful.add("plugins", "--apache", action="store_true",
help="Obtain and install certs using Apache")
helpful.add("plugins", "--nginx", action="store_true",
help="Obtain and install certs using Nginx")
helpful.add("plugins", "--standalone", action="store_true",
2015-10-20 17:02:34 -04:00
help='Obtain certs using a "standalone" webserver.')
2015-11-17 20:11:03 -05:00
helpful.add("plugins", "--manual", action="store_true",
help='Provide laborious manual instructions for obtaining a cert')
2015-11-17 19:29:06 -05:00
helpful.add("plugins", "--webroot", action="store_true",
help='Obtain certs by placing files in a webroot directory.')
# things should not be reorder past/pre this comment:
# plugins_group should be displayed in --help before plugin
# specific groups (so that plugins_group.description makes sense)
helpful.add_plugin_args(plugins)
2015-11-21 13:59:05 -05:00
# These would normally be a flag within the webroot plugin, but because
# they are parsed in conjunction with --domains, they live here for
2015-12-18 21:27:24 -05:00
# legibility. helpful.add_plugin_ags must be called first to add the
2015-11-21 13:59:05 -05:00
# "webroot" topic
helpful.add("webroot", "-w", "--webroot-path", default=[], action=WebrootPathProcessor,
2015-11-30 21:24:40 -05:00
help="public_html / webroot path. This can be specified multiple times to "
"handle different domains; each domain will have the webroot path that"
2015-12-02 11:16:23 -05:00
" preceded it. For instance: `-w /var/www/example -d example.com -d "
2015-11-30 21:24:40 -05:00
"www.example.com -w /var/www/thing -d thing.net -d m.thing.net`")
# --webroot-map still has some awkward properties, so it is undocumented
helpful.add("webroot", "--webroot-map", default={}, action=WebrootMapProcessor,
2016-02-08 19:11:20 -05:00
help="JSON dictionary mapping domains to webroot paths; this "
"implies -d for each entry. You may need to escape this "
2016-02-08 20:48:12 -05:00
"from your shell. E.g.: --webroot-map "
"""'{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """
2016-02-08 19:11:20 -05:00
"This option is merged with, but takes precedence over, "
"-w / -d entries. At present, if you put webroot-map in "
"a config file, it needs to be on a single line, like: "
'webroot-map = {"example.com":"/var/www"}.')
class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring
def __init__(self, *args, **kwargs):
self.domain_before_webroot = False
argparse.Action.__init__(self, *args, **kwargs)
def __call__(self, parser, args, webroot, option_string=None):
"""
Keep a record of --webroot-path / -w flags during processing, so that
we know which apply to which -d flags
"""
if not args.webroot_path: # first -w flag encountered
# if any --domain flags preceded the first --webroot-path flag,
# apply that webroot path to those; subsequent entries in
# args.webroot_map are filled in by cli.DomainFlagProcessor
if args.domains:
self.domain_before_webroot = True
for d in args.domains:
args.webroot_map.setdefault(d, webroot)
elif self.domain_before_webroot:
# FIXME if you set domains in a args file, you should get a different error
2015-12-01 19:33:35 -05:00
# here, pointing you to --webroot-map
raise errors.Error("If you specify multiple webroot paths, one of "
"them must precede all domain flags")
args.webroot_path.append(webroot)
2016-02-29 02:34:44 -05:00
def process_domain(args_or_config, domain_arg, webroot_path=None):
"""
Process a new -d flag, helping the webroot plugin construct a map of
{domain : webrootpath} if -w / --webroot-path is in use
:param args_or_config: may be an argparse args object, or a NamespaceConfig object
:param str domain_arg: a string representing 1+ domains, eg: "eg.is, example.com"
:param str webroot_path: (optional) the webroot_path for these domains
"""
webroot_path = webroot_path if webroot_path else args_or_config.webroot_path
for domain in (d.strip() for d in domain_arg.split(",")):
2016-02-01 22:35:18 -05:00
domain = le_util.enforce_domain_sanity(domain)
if domain not in args_or_config.domains:
args_or_config.domains.append(domain)
# Each domain has a webroot_path of the most recent -w flag
# unless it was explicitly included in webroot_map
if webroot_path:
args_or_config.webroot_map.setdefault(domain, webroot_path[-1])
2016-02-08 19:11:20 -05:00
class WebrootMapProcessor(argparse.Action): # pylint: disable=missing-docstring
def __call__(self, parser, args, webroot_map_arg, option_string=None):
webroot_map = json.loads(webroot_map_arg)
for domains, webroot_path in six.iteritems(webroot_map):
2016-02-29 02:34:44 -05:00
process_domain(args, domains, [webroot_path])
2016-02-08 19:11:20 -05:00
class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring
def __call__(self, parser, args, domain_arg, option_string=None):
2016-02-29 02:34:44 -05:00
"""Just wrap process_domain in argparseese."""
process_domain(args, domain_arg)