diff --git a/.pylintrc b/.pylintrc index 5302133ad..825699036 100644 --- a/.pylintrc +++ b/.pylintrc @@ -38,7 +38,7 @@ load-plugins=linter_plugin # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=fixme,locally-disabled,abstract-class-not-used +disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods # abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1) @@ -101,7 +101,7 @@ function-rgx=[a-z_][a-z0-9_]{2,40}$ function-name-hint=[a-z_][a-z0-9_]{2,40}$ # Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ +variable-rgx=[a-z_][a-z0-9_]{1,30}$ # Naming hint for variable names variable-name-hint=[a-z_][a-z0-9_]{2,30}$ @@ -228,7 +228,8 @@ max-module-lines=1250 indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 +# This does something silly/broken... +#indent-after-paren=4 [TYPECHECK] diff --git a/CHANGES.rst b/CHANGES.rst index 741d9bc7c..3ed13041b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ ChangeLog Please note: the change log will only get updated after first release - for now please use the -`commit log `_. +`commit log `_. Release 0.1.0 (not released yet) diff --git a/Dockerfile b/Dockerfile index 78aa7a75b..479aa4e85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# https://github.com/letsencrypt/lets-encrypt-preview/pull/431#issuecomment-103659297 +# https://github.com/letsencrypt/letsencrypt/pull/431#issuecomment-103659297 # it is more likely developers will already have ubuntu:trusty rather # than e.g. debian:jessie and image size differences are negligible FROM ubuntu:trusty diff --git a/LICENSE.txt b/LICENSE.txt index d3c19bbd1..5a9f6fa55 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Let's Encrypt Preview: +Let's Encrypt: Copyright (c) Internet Security Research Group Licensed Apache Version 2.0 diff --git a/README.rst b/README.rst index 784e06436..7c98999e8 100644 --- a/README.rst +++ b/README.rst @@ -45,16 +45,16 @@ server automatically!:: :target: https://travis-ci.org/letsencrypt/letsencrypt :alt: Travis CI status -.. |coverage| image:: https://coveralls.io/repos/letsencrypt/lets-encrypt-preview/badge.svg?branch=master - :target: https://coveralls.io/r/letsencrypt/lets-encrypt-preview +.. |coverage| image:: https://coveralls.io/repos/letsencrypt/letsencrypt/badge.svg?branch=master + :target: https://coveralls.io/r/letsencrypt/letsencrypt :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ :target: https://readthedocs.org/projects/letsencrypt/ :alt: Documentation status -.. |container| image:: https://quay.io/repository/letsencrypt/lets-encrypt-preview/status - :target: https://quay.io/repository/letsencrypt/lets-encrypt-preview +.. |container| image:: https://quay.io/repository/letsencrypt/letsencrypt/status + :target: https://quay.io/repository/letsencrypt/letsencrypt :alt: Docker Repository on Quay.io .. _`installation instructions`: diff --git a/docs/contributing.rst b/docs/contributing.rst index 804cec95c..05a6875fe 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -97,7 +97,7 @@ Configurators may implement just one of those). There are also `~letsencrypt.interfaces.IDisplay` plugins, which implement bindings to alternative UI libraries. -.. _interfaces.py: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/letsencrypt/interfaces.py +.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py Authenticators diff --git a/docs/pkgs.rst b/docs/pkgs.rst index 8119ffc7e..2e1b18dfb 100644 --- a/docs/pkgs.rst +++ b/docs/pkgs.rst @@ -6,7 +6,7 @@ Packages described in `#358`_. For the time being those packages are bundled together into a single repo, and single documentation. -.. _`#358`: https://github.com/letsencrypt/lets-encrypt-preview/issues/358 +.. _`#358`: https://github.com/letsencrypt/letsencrypt/issues/358 .. toctree:: :glob: diff --git a/docs/using.rst b/docs/using.rst index 96eb62b05..951c991d2 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -14,7 +14,7 @@ server that the domain your requesting a cert for resolves to, sudo docker run -it --rm -p 443:443 --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ - quay.io/letsencrypt/lets-encrypt-preview:latest + quay.io/letsencrypt/letsencrypt:latest and follow the instructions. Your new cert will be available in ``/etc/letsencrypt/certs``. @@ -30,8 +30,8 @@ Please `install Git`_ and run the following commands: .. code-block:: shell - git clone https://github.com/letsencrypt/lets-encrypt-preview - cd lets-encrypt-preview + git clone https://github.com/letsencrypt/letsencrypt + cd letsencrypt Alternatively you could `download the ZIP archive`_ and extract the snapshot of our repository, but it's strongly recommended to use the @@ -39,7 +39,7 @@ above method instead. .. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git .. _`download the ZIP archive`: - https://github.com/letsencrypt/lets-encrypt-preview/archive/master.zip + https://github.com/letsencrypt/letsencrypt/archive/master.zip Prerequisites @@ -76,7 +76,7 @@ For squeeze you will need to: - Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``. -.. _`#280`: https://github.com/letsencrypt/lets-encrypt-preview/issues/280 +.. _`#280`: https://github.com/letsencrypt/letsencrypt/issues/280 Mac OSX diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 3bdf2bfc6..bdc287370 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -25,9 +25,49 @@ from letsencrypt import reporter from letsencrypt.display import util as display_util from letsencrypt.display import ops as display_ops - from letsencrypt.plugins import disco as plugins_disco +# 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 = """ + letsencrypt [SUBCOMMAND] [options] [domains] + +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 +the cert. """ + +# This is the short help for letsencrypt --help, where we disable argparse +# altogether +USAGE = SHORT_USAGE + """Major SUBCOMMANDS are: + + (default) everything Obtain & install a cert in your current webserver + auth Authenticate & obtain cert, but do not install it + install Install a previously obtained cert in a server + revoke Revoke a previously obtained certificate + rollback Rollback server configuration changes made during install + config-changes Show changes made to server config during installation + +Choice of server for authentication/installation: + + --apache Use the Apache plugin for authentication & installation + --nginx Use the Nginx plugin for authentication & installation + --standalone Run a standalone HTTPS server (for authentication only) + OR: + --authenticator standalone --installer nginx + +More detailed help: + + -h, --help [topic] print this message, or detailed help on a topic; + the available topics are: + + all, apache, automation, nginx, paths, security, testing, or any of the + sucommands +""" + + def _account_init(args, config): le_util.make_or_verify_dir( @@ -110,7 +150,7 @@ def run(args, config, plugins): def auth(args, config, plugins): - """Obtain a certificate (no install).""" + """Authenticate & obtain cert, but do not install it""" # XXX: Update for renewer / RenewableCert acc = _account_init(args, config) if acc is None: @@ -135,7 +175,7 @@ def auth(args, config, plugins): def install(args, config, plugins): - """Install (no auth).""" + """Install a previously obtained cert in a server""" # XXX: Update for renewer/RenewableCert acc = _account_init(args, config) if acc is None: @@ -152,7 +192,7 @@ def install(args, config, plugins): def revoke(args, unused_config, unused_plugins): - """Revoke.""" + """Revoke a previously obtained certificate""" if args.rev_cert is None and args.rev_key is None: return "At least one of --certificate or --key is required" @@ -164,12 +204,12 @@ def revoke(args, unused_config, unused_plugins): def rollback(args, config, plugins): - """Rollback.""" + """Rollback server configuration changes made during install""" client.rollback(args.installer, args.checkpoints, config, plugins) def config_changes(unused_args, config, unused_plugins): - """View config changes. + """Show changes made to server config during installation View checkpoints and associated configuration changes. @@ -178,7 +218,7 @@ def config_changes(unused_args, config, unused_plugins): def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print - """List plugins.""" + """List server software plugins""" logging.debug("Expected interfaces: %s", args.ifaces) ifaces = [] if args.ifaces is None else args.ifaces @@ -224,51 +264,168 @@ def flag_default(name): """Default value for CLI flag.""" return constants.CLI_DEFAULTS[name] -def config_help(name): +def config_help(name, hidden=False): """Help message for `.IConfig` attribute.""" - return interfaces.IConfig[name].__doc__ + if hidden: + return argparse.SUPPRESS + else: + return interfaces.IConfig[name].__doc__ + +class SilentParser(object): + """An a mini parser wrapper that doesn't print help for its + arguments... this one is just needed to the use of callbacks to define + arguments within plugins""" + def __init__(self, parser): + self.parser = parser + def add_argument(self, *args, **kwargs): + """Wrap, but silence help""" + kwargs["help"] = argparse.SUPPRESS + self.parser.add_argument(*args, **kwargs) + +HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] +class HelpfulArgumentParser(object): + """This class wraps argparse, adding the ability to make --help less + verbose, and request help on specific subcategories at a time, eg + 'letsencrypt --help security' for security options.""" + + def __init__(self, args, plugins): + self.args = args + plugin_names = [name for name, _p in plugins.iteritems()] + self.help_topics = HELP_TOPICS + plugin_names + self.parser = configargparse.ArgParser( + usage=SHORT_USAGE, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + args_for_setting_config_path=["-c", "--config"], + default_config_files=flag_default("config_files")) + + # This is the only way to turn off overly verbose config flag documentation + self.parser._add_config_file_help = False # pylint: disable=protected-access + self.silent_parser = SilentParser(self.parser) + + help1 = self.prescan_for_flag("-h", self.help_topics) + help2 = self.prescan_for_flag("--help", self.help_topics) + assert max(True, "a") == "a", "Gravity changed direction" + help_arg = max(help1, help2) + if help_arg == True: + # just --help with no topic; avoid argparse altogether + print USAGE + sys.exit(0) + self.visible_topics = self.determine_help_topics(help_arg) + #print self.visible_topics + self.groups = {} # elements are added by .add_group() + self.add_plugin_args(plugins) + + def prescan_for_flag(self, flag, possible_arguments): + """check for a flag, which accepts a fixed set of possible arguments, in + the command line; we will use this information to configure argparse's + help correctly. Return the flag's argument, if it has one that matches + the sequence @possible_arguments; otherwise return whether the flag is + present""" + if flag not in self.args: + return False + pos = self.args.index(flag) + try: + nxt = self.args[pos + 1] + if nxt in possible_arguments: + return nxt + except IndexError: + pass + return True + + def add(self, topic, *args, **kwargs): + """Add a new command line argument. @topic is required, to indicate + which part of the help will document it, but can be None for `always + documented'.""" + + if topic and self.visible_topics[topic]: + group = self.groups[topic] + group.add_argument(*args, **kwargs) + else: + kwargs["help"] = argparse.SUPPRESS + self.parser.add_argument(*args, **kwargs) + + def add_group(self, topic, **kwargs): + """This has to be called once for every topic; but we leave those calls + next to the argument definitions for clarity. Return something + arguments can be added to if necessary, either the parser or an argument + group.""" + if self.visible_topics[topic]: + #print "Adding visible group " + topic + group = self.parser.add_argument_group(topic, **kwargs) + self.groups[topic] = group + return group + else: + #print "Invisible group " + topic + return self.silent_parser + + 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.""" + # TODO: plugin_parser should be called for every detected plugin + for name, plugin_ep in plugins.iteritems(): + parser_or_group = self.add_group(name, description=plugin_ep.description) + #print parser_or_group + 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 dislpay. @chosen_topic has prescan_for_flag's return type""" + + # topics maps each topic to whether it should be documented by + # argparse on the command line + if chosen_topic == "all": + return dict([(t, True) for t in self.help_topics]) + elif not chosen_topic: + return dict([(t, False) for t in self.help_topics]) + else: + return dict([(t, t == chosen_topic) for t in self.help_topics]) -def create_parser(plugins): +def create_parser(plugins, args): """Create parser.""" - parser = configargparse.ArgParser( - description=__doc__, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - args_for_setting_config_path=["-c", "--config"], - default_config_files=flag_default("config_files")) - add = parser.add_argument + helpful = HelpfulArgumentParser(args, plugins) - # --help is automatically provided by argparse - add("--version", action="version", version="%(prog)s {0}".format( - letsencrypt.__version__)) - add("-v", "--verbose", dest="verbose_count", action="count", + 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.") - add("--no-confirm", dest="no_confirm", action="store_true", + # --help is automatically provided by argparse + + helpful.add_group("automation", + description="Arguments for automating execution & other tweaks") + helpful.add( + "automation", "--version", action="version", + version="%(prog)s {0}".format(letsencrypt.__version__), + help="show program's version number and exit") + helpful.add( + "automation", "--no-confirm", dest="no_confirm", action="store_true", help="Turn off confirmation screens, currently used for --revoke") - add("-e", "--agree-tos", dest="tos", action="store_true", - help="Skip the end user license agreement screen.") - add("-t", "--text", dest="text_mode", action="store_true", + helpful.add( + "automation", "--agree-eula", "-e", dest="tos", action="store_true", + help="Agree to the Let's Encrypt Subscriber Agreement") + helpful.add( + None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") - add("--no-simple-http-tls", action="store_true", - help=config_help("no_simple_http_tls")) - testing_group = parser.add_argument_group( + 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!") - testing_group.add_argument( - "--no-verify-ssl", action="store_true", + helpful.add( + "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) # TODO: apache and nginx plugins do NOT respect it - testing_group.add_argument( - "--dvsni-port", type=int, help=config_help("dvsni_port"), - default=flag_default("dvsni_port")) + helpful.add( + "testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"), + help=config_help("dvsni_port")) - subparsers = parser.add_subparsers(metavar="SUBCOMMAND") + helpful.add("testing", "--no-simple-http-tls", action="store_true", + help=config_help("no_simple_http_tls")) + + subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") def add_subparser(name, func): # pylint: disable=missing-docstring subparser = subparsers.add_parser( name, help=func.__doc__.splitlines()[0], description=func.__doc__) @@ -292,25 +449,27 @@ def create_parser(plugins): "--installers", action="append_const", dest="ifaces", const=interfaces.IInstaller) - parser.add_argument("--configurator") - parser.add_argument("-a", "--authenticator") - parser.add_argument("-i", "--installer") + helpful.add(None, "--configurator") + helpful.add(None, "-a", "--authenticator") + helpful.add(None, "-i", "--installer") # 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") - add("-d", "--domains", metavar="DOMAIN", action="append") - add("-s", "--server", default=flag_default("server"), - help=config_help("server")) - add("-k", "--authkey", type=read_file, - help="Path to the authorized key file") - add("-m", "--email", help=config_help("email")) - add("-B", "--rsa-key-size", type=int, metavar="N", - default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) + helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append") + helpful.add(None, "-k", "--accountkey", type=read_file, + help="Path to the account key file") + helpful.add(None, "-m", "--email", help=config_help("email")) + + helpful.add_group( + "security", description="Security parameters & server settings") + helpful.add("security", "-B", "--rsa-key-size", type=int, metavar="N", + default=flag_default("rsa_key_size"), + help=config_help("rsa_key_size")) # TODO: resolve - assumes binary logic while client.py assumes ternary. - add("-r", "--redirect", action="store_true", + helpful.add("security", "-r", "--redirect", action="store_true", help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") @@ -326,48 +485,41 @@ def create_parser(plugins): default=flag_default("rollback_checkpoints"), help="Revert configuration N number of checkpoints.") - _paths_parser(parser.add_argument_group("paths")) - - # TODO: plugin_parser should be called for every detected plugin - for name, plugin_ep in plugins.iteritems(): - plugin_ep.plugin_cls.inject_parser_options( - parser.add_argument_group( - name, description=plugin_ep.description), name) - - return parser + _paths_parser(helpful) -def _paths_parser(parser): - add = parser.add_argument - add("--config-dir", default=flag_default("config_dir"), + return helpful.parser + + +def _paths_parser(helpful): + add = helpful.add + helpful.add_group("paths", description="Arguments changing execution paths & servers") + add("paths", "--config-dir", default=flag_default("config_dir"), help=config_help("config_dir")) - add("--work-dir", default=flag_default("work_dir"), + add("paths", "--work-dir", default=flag_default("work_dir"), help=config_help("work_dir")) - add("--backup-dir", default=flag_default("backup_dir"), + add("paths", "--backup-dir", default=flag_default("backup_dir"), help=config_help("backup_dir")) - add("--key-dir", default=flag_default("key_dir"), + add("paths", "--key-dir", default=flag_default("key_dir"), help=config_help("key_dir")) - add("--cert-dir", default=flag_default("certs_dir"), + add("paths", "--cert-dir", default=flag_default("certs_dir"), help=config_help("cert_dir")) - - add("--le-vhost-ext", default="-le-ssl.conf", + add("paths", "--le-vhost-ext", default="-le-ssl.conf", help=config_help("le_vhost_ext")) - add("--cert-path", default=flag_default("cert_path"), + add("paths", "--cert-path", default=flag_default("cert_path"), help=config_help("cert_path")) - add("--chain-path", default=flag_default("chain_path"), + add("paths", "--chain-path", default=flag_default("chain_path"), help=config_help("chain_path")) - - add("--renewer-config-file", default=flag_default("renewer_config_file"), + add("paths", "--renewer-config-file", default=flag_default("renewer_config_file"), help=config_help("renewer_config_file")) - - return parser - + add("paths", "-s", "--server", default=flag_default("server"), + help=config_help("server")) def main(args=sys.argv[1:]): """Command line argument parsing and main script execution.""" # note: arg parser internally handles --help (and exits afterwards) plugins = plugins_disco.PluginsRegistry.find_all() - args = create_parser(plugins).parse_args(args) + args = create_parser(plugins, args).parse_args(args) config = configuration.NamespaceConfig(args) # Displayer diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index e5654a03d..0f8207b7a 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -33,8 +33,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): if exception.errno == errno.EEXIST: if not check_permissions(directory, mode, uid): raise errors.Error( - "%s exists, but does not have the proper " - "permissions or owner" % directory) + "%s exists, this client can't access it" % directory) else: raise