mirror of
https://github.com/certbot/certbot.git
synced 2026-01-21 19:01:07 +03:00
Merge pull request #376 from kuba/cli
Plugins discovery and new CLI with subcommands
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,4 +10,4 @@ m3
|
||||
*~
|
||||
.vagrant
|
||||
*.swp
|
||||
\#*#
|
||||
\#*#
|
||||
10
README.rst
10
README.rst
@@ -17,11 +17,15 @@ It's all automated:
|
||||
* If domain control has been proven, a certificate will get issued and the tool
|
||||
will automatically install it.
|
||||
|
||||
All you need to do is:
|
||||
All you need to do is::
|
||||
|
||||
::
|
||||
user@www:~$ sudo letsencrypt -d www.example.org auth
|
||||
|
||||
user@www:~$ sudo letsencrypt -d www.example.org
|
||||
and if you have a compatbile web server (Apache), Let's Encrypt can
|
||||
not only get a new certificate, but also deploy it and configure your
|
||||
server automatically!::
|
||||
|
||||
user@www:~$ sudo letsencrypt -d www.example.org run
|
||||
|
||||
|
||||
**Encrypt ALL the things!**
|
||||
|
||||
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@@ -10,7 +10,7 @@ cd /vagrant
|
||||
sudo ./bootstrap/ubuntu.sh
|
||||
if [ ! -d "venv" ]; then
|
||||
virtualenv --no-site-packages -p python2 venv
|
||||
./venv/bin/python setup.py dev
|
||||
./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing]
|
||||
fi
|
||||
SETUP_SCRIPT
|
||||
|
||||
|
||||
5
docs/api/client/plugins/common.rst
Normal file
5
docs/api/client/plugins/common.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.plugins.common`
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.common
|
||||
:members:
|
||||
5
docs/api/client/plugins/disco.rst
Normal file
5
docs/api/client/plugins/disco.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.plugins.disco`
|
||||
---------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.disco
|
||||
:members:
|
||||
@@ -15,7 +15,7 @@ Now you can install the development packages:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
./venv/bin/python setup.py dev
|
||||
./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing]
|
||||
|
||||
The code base, including your pull requests, **must** have 100% test
|
||||
statement coverage **and** be compliant with the :ref:`coding style
|
||||
@@ -48,7 +48,7 @@ synced to ``/vagrant``, so you can get started with:
|
||||
|
||||
vagrant ssh
|
||||
cd /vagrant
|
||||
./venv/bin/python setup.py install
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
sudo ./venv/bin/letsencrypt
|
||||
|
||||
Support for other Linux distributions coming soon.
|
||||
|
||||
@@ -53,8 +53,7 @@ Installation
|
||||
.. code-block:: shell
|
||||
|
||||
virtualenv --no-site-packages -p python2 venv
|
||||
./venv/bin/python setup.py install
|
||||
sudo ./venv/bin/letsencrypt
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
|
||||
|
||||
Usage
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
"""Example Let's Encrypt plugins."""
|
||||
"""Example Let's Encrypt plugins.
|
||||
|
||||
For full examples, see `letsencrypt.client.plugins`.
|
||||
|
||||
"""
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client.plugins import common
|
||||
|
||||
|
||||
class Authenticator(object):
|
||||
class Authenticator(common.Plugin):
|
||||
"""Example Authenticator."""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = 'Example Authenticator plugin'
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
description = "Example Authenticator plugin"
|
||||
|
||||
# Implement all methods from IAuthenticator, remembering to add
|
||||
# "self" as first argument, e.g. def prepare(self)...
|
||||
|
||||
# For full examples, see letsencrypt.client.plugins
|
||||
|
||||
class Installer(common.Plugins):
|
||||
"""Example Installer."""
|
||||
zope.interface.implements(interfaces.IInstaller)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Example Installer plugin"
|
||||
|
||||
# Implement all methods from IInstaller, remembering to add
|
||||
# "self" as first argument, e.g. def get_all_names(self)...
|
||||
|
||||
@@ -9,8 +9,9 @@ setup(
|
||||
'zope.interface',
|
||||
],
|
||||
entry_points={
|
||||
'letsencrypt.authenticators': [
|
||||
'example = letsencrypt_example_plugins:Authenticator',
|
||||
'letsencrypt.plugins': [
|
||||
'example_authenticator = letsencrypt_example_plugins:Authenticator',
|
||||
'example_installer = letsencrypt_example_plugins:Installer',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,9 +4,10 @@ import logging
|
||||
import augeas
|
||||
|
||||
from letsencrypt.client import reverter
|
||||
from letsencrypt.client.plugins import common
|
||||
|
||||
|
||||
class AugeasConfigurator(object):
|
||||
class AugeasConfigurator(common.Plugin):
|
||||
"""Base Augeas Configurator class.
|
||||
|
||||
:ivar config: Configuration.
|
||||
@@ -21,8 +22,8 @@ class AugeasConfigurator(object):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AugeasConfigurator, self).__init__(*args, **kwargs)
|
||||
|
||||
# Set Augeas flags to not save backup (we do it ourselves)
|
||||
# Set Augeas to not load anything by default
|
||||
@@ -34,7 +35,7 @@ class AugeasConfigurator(object):
|
||||
# This needs to occur before VirtualHost objects are setup...
|
||||
# because this will change the underlying configuration and potential
|
||||
# vhosts
|
||||
self.reverter = reverter.Reverter(config)
|
||||
self.reverter = reverter.Reverter(self.config)
|
||||
self.reverter.recovery_routine()
|
||||
|
||||
def check_parsing_errors(self, lens):
|
||||
|
||||
373
letsencrypt/client/cli.py
Normal file
373
letsencrypt/client/cli.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""Let's Encrypt CLI."""
|
||||
# TODO: Sanity check all input. Be sure to avoid shell code etc...
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import configargparse
|
||||
import zope.component
|
||||
import zope.interface.exceptions
|
||||
import zope.interface.verify
|
||||
|
||||
import letsencrypt
|
||||
|
||||
from letsencrypt.client import account
|
||||
from letsencrypt.client import configuration
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client import client
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import log
|
||||
|
||||
from letsencrypt.client.display import util as display_util
|
||||
from letsencrypt.client.display import ops as display_ops
|
||||
|
||||
from letsencrypt.client.plugins import disco as plugins_disco
|
||||
|
||||
|
||||
def _account_init(args, config):
|
||||
le_util.make_or_verify_dir(
|
||||
config.config_dir, constants.CONFIG_DIRS_MODE, os.geteuid())
|
||||
|
||||
# Prepare for init of Client
|
||||
if args.email is None:
|
||||
return client.determine_account(config)
|
||||
else:
|
||||
try:
|
||||
# The way to get the default would be args.email = ""
|
||||
# First try existing account
|
||||
return account.Account.from_existing_account(config, args.email)
|
||||
except errors.LetsEncryptClientError:
|
||||
try:
|
||||
# Try to make an account based on the email address
|
||||
return account.Account.from_email(config, args.email)
|
||||
except errors.LetsEncryptClientError:
|
||||
return None
|
||||
|
||||
|
||||
def _common_run(args, config, acc, authenticator, installer):
|
||||
if args.domains is None:
|
||||
doms = display_ops.choose_names(installer)
|
||||
else:
|
||||
doms = args.domains
|
||||
|
||||
if not doms:
|
||||
sys.exit("Please specify --domains, or --installer that will "
|
||||
"help in domain names autodiscovery")
|
||||
|
||||
acme = client.Client(config, acc, authenticator, installer)
|
||||
|
||||
# Validate the key and csr
|
||||
client.validate_key_csr(acc.key)
|
||||
|
||||
if authenticator is not None:
|
||||
if acc.regr is None:
|
||||
try:
|
||||
acme.register()
|
||||
except errors.LetsEncryptClientError:
|
||||
sys.exit("Unable to register an account with ACME server")
|
||||
|
||||
return acme, doms
|
||||
|
||||
|
||||
def run(args, config, plugins):
|
||||
"""Obtain a certificate and install."""
|
||||
acc = _account_init(args, config)
|
||||
if acc is None:
|
||||
return None
|
||||
|
||||
if args.configurator is not None and (args.installer is not None or
|
||||
args.authenticator is not None):
|
||||
return ("Either --configurator or --authenticator/--installer"
|
||||
"pair, but not both, is allowed")
|
||||
|
||||
if args.authenticator is not None or args.installer is not None:
|
||||
installer = display_ops.pick_installer(
|
||||
config, args.installer, plugins)
|
||||
authenticator = display_ops.pick_authenticator(
|
||||
config, args.authenticator, plugins)
|
||||
else:
|
||||
# TODO: this assume that user doesn't want to pick authenticator
|
||||
# and installer separately...
|
||||
authenticator = installer = display_ops.pick_configurator(
|
||||
config, args.configurator, plugins)
|
||||
|
||||
if installer is None or authenticator is None:
|
||||
return "Configurator could not be determined"
|
||||
|
||||
acme, doms = _common_run(args, config, acc, authenticator, installer)
|
||||
cert_key, cert_path, chain_path = acme.obtain_certificate(doms)
|
||||
acme.deploy_certificate(doms, cert_key, cert_path, chain_path)
|
||||
acme.enhance_config(doms, args.redirect)
|
||||
|
||||
|
||||
def auth(args, config, plugins):
|
||||
"""Obtain a certificate (no install)."""
|
||||
acc = _account_init(args, config)
|
||||
if acc is None:
|
||||
return None
|
||||
|
||||
authenticator = display_ops.pick_authenticator(
|
||||
config, args.authenticator, plugins)
|
||||
if authenticator is None:
|
||||
return "Authenticator could not be determined"
|
||||
|
||||
if args.installer is not None:
|
||||
installer = display_ops.pick_installer(config, args.installer, plugins)
|
||||
else:
|
||||
installer = None
|
||||
|
||||
acme, doms = _common_run(
|
||||
args, config, acc, authenticator=authenticator, installer=installer)
|
||||
acme.obtain_certificate(doms)
|
||||
|
||||
|
||||
def install(args, config, plugins):
|
||||
"""Install (no auth)."""
|
||||
acc = _account_init(args, config)
|
||||
if acc is None:
|
||||
return None
|
||||
|
||||
installer = display_ops.pick_installer(config, args.installer, plugins)
|
||||
if installer is None:
|
||||
return "Installer could not be determined"
|
||||
acme, doms = _common_run(
|
||||
args, config, acc, authenticator=None, installer=installer)
|
||||
assert args.cert_path is not None
|
||||
acme.deploy_certificate(doms, acc.key, args.cert_path, args.chain_path)
|
||||
acme.enhance_config(doms, args.redirect)
|
||||
|
||||
|
||||
def revoke(args, unused_config, unused_plugins):
|
||||
"""Revoke."""
|
||||
if args.rev_cert is None and args.rev_key is None:
|
||||
return "At least one of --certificate or --key is required"
|
||||
|
||||
# This depends on the renewal config and cannot be completed yet.
|
||||
zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
"Revocation is not available with the new Boulder server yet.")
|
||||
#client.revoke(args.installer, config, plugins, args.no_confirm,
|
||||
# args.rev_cert, args.rev_key)
|
||||
|
||||
|
||||
def rollback(args, config, plugins):
|
||||
"""Rollback."""
|
||||
client.rollback(args.installer, args.checkpoints, config, plugins)
|
||||
|
||||
|
||||
def config_changes(unused_args, config, unused_plugins):
|
||||
"""View config changes.
|
||||
|
||||
View checkpoints and associated configuration changes.
|
||||
|
||||
"""
|
||||
client.view_config_changes(config)
|
||||
|
||||
|
||||
def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print
|
||||
"""List plugins."""
|
||||
logging.debug("Expected interfaces: %s", args.ifaces)
|
||||
|
||||
ifaces = [] if args.ifaces is None else args.ifaces
|
||||
filtered = plugins.ifaces(ifaces)
|
||||
logging.debug("Filtered plugins: %r", filtered)
|
||||
|
||||
if not args.init and not args.prepare:
|
||||
print str(filtered)
|
||||
return
|
||||
|
||||
filtered.init(config)
|
||||
verified = filtered.verify(ifaces)
|
||||
logging.debug("Verified plugins: %r", verified)
|
||||
|
||||
if not args.prepare:
|
||||
print str(verified)
|
||||
return
|
||||
|
||||
verified.prepare()
|
||||
available = verified.available()
|
||||
logging.debug("Prepared plugins: %s", available)
|
||||
print str(available)
|
||||
|
||||
|
||||
def read_file(filename):
|
||||
"""Returns the given file's contents with universal new line support.
|
||||
|
||||
:param str filename: Filename
|
||||
|
||||
:returns: A tuple of filename and its contents
|
||||
:rtype: tuple
|
||||
|
||||
:raises argparse.ArgumentTypeError: File does not exist or is not readable.
|
||||
|
||||
"""
|
||||
try:
|
||||
return filename, open(filename, "rU").read()
|
||||
except IOError as exc:
|
||||
raise argparse.ArgumentTypeError(exc.strerror)
|
||||
|
||||
|
||||
def flag_default(name):
|
||||
"""Default value for CLI flag."""
|
||||
return constants.CLI_DEFAULTS[name]
|
||||
|
||||
def config_help(name):
|
||||
"""Help message for `.IConfig` attribute."""
|
||||
return interfaces.IConfig[name].__doc__
|
||||
|
||||
|
||||
def create_parser(plugins):
|
||||
"""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
|
||||
|
||||
# --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",
|
||||
default=flag_default("verbose_count"))
|
||||
add("--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",
|
||||
help="Use the text output instead of the curses UI.")
|
||||
|
||||
subparsers = 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__)
|
||||
subparser.set_defaults(func=func)
|
||||
return subparser
|
||||
|
||||
add_subparser("run", run)
|
||||
add_subparser("auth", auth)
|
||||
add_subparser("install", install)
|
||||
parser_revoke = add_subparser("revoke", revoke)
|
||||
parser_rollback = add_subparser("rollback", rollback)
|
||||
add_subparser("config_changes", config_changes)
|
||||
|
||||
parser_plugins = add_subparser("plugins", plugins_cmd)
|
||||
parser_plugins.add_argument("--init", action="store_true")
|
||||
parser_plugins.add_argument("--prepare", action="store_true")
|
||||
parser_plugins.add_argument(
|
||||
"--authenticators", action="append_const", dest="ifaces",
|
||||
const=interfaces.IAuthenticator)
|
||||
parser_plugins.add_argument(
|
||||
"--installers", action="append_const", dest="ifaces",
|
||||
const=interfaces.IInstaller)
|
||||
|
||||
parser.add_argument("--configurator")
|
||||
parser.add_argument("-a", "--authenticator")
|
||||
parser.add_argument("-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"))
|
||||
# TODO: resolve - assumes binary logic while client.py assumes ternary.
|
||||
add("-r", "--redirect", action="store_true",
|
||||
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
|
||||
"authenticated vhost.")
|
||||
|
||||
parser_revoke.add_argument(
|
||||
"--certificate", dest="rev_cert", type=read_file, metavar="CERT_PATH",
|
||||
help="Revoke a specific certificate.")
|
||||
parser_revoke.add_argument(
|
||||
"--key", dest="rev_key", type=read_file, metavar="KEY_PATH",
|
||||
help="Revoke all certs generated by the provided authorized key.")
|
||||
|
||||
parser_rollback.add_argument(
|
||||
"--checkpoints", type=int, metavar="N",
|
||||
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
|
||||
|
||||
|
||||
def _paths_parser(parser):
|
||||
add = parser.add_argument
|
||||
add("--config-dir", default=flag_default("config_dir"),
|
||||
help=config_help("config_dir"))
|
||||
add("--work-dir", default=flag_default("work_dir"),
|
||||
help=config_help("work_dir"))
|
||||
add("--backup-dir", default=flag_default("backup_dir"),
|
||||
help=config_help("backup_dir"))
|
||||
add("--key-dir", default=flag_default("key_dir"),
|
||||
help=config_help("key_dir"))
|
||||
add("--cert-dir", default=flag_default("certs_dir"),
|
||||
help=config_help("cert_dir"))
|
||||
|
||||
add("--le-vhost-ext", default="-le-ssl.conf",
|
||||
help=config_help("le_vhost_ext"))
|
||||
add("--cert-path", default=flag_default("cert_path"),
|
||||
help=config_help("cert_path"))
|
||||
add("--chain-path", default=flag_default("chain_path"),
|
||||
help=config_help("chain_path"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
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)
|
||||
config = configuration.NamespaceConfig(args)
|
||||
|
||||
# Displayer
|
||||
if args.text_mode:
|
||||
displayer = display_util.FileDisplay(sys.stdout)
|
||||
else:
|
||||
displayer = display_util.NcursesDisplay()
|
||||
zope.component.provideUtility(displayer)
|
||||
|
||||
# Logging
|
||||
level = -args.verbose_count * 10
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(level)
|
||||
logging.debug("Logging level set at %d", level)
|
||||
if not args.text_mode:
|
||||
logger.addHandler(log.DialogHandler())
|
||||
|
||||
logging.debug("Discovered plugins: %r", plugins)
|
||||
|
||||
if not os.geteuid() == 0:
|
||||
logging.warning(
|
||||
"Root (sudo) is required to run most of letsencrypt functionality.")
|
||||
# check must be done after arg parsing as --help should work
|
||||
# w/o root; on the other hand, e.g. "letsencrypt run
|
||||
# --authenticator dns" or "letsencrypt plugins" does not
|
||||
# require root as well
|
||||
#return (
|
||||
# "{0}Root is required to run letsencrypt. Please use sudo.{0}"
|
||||
# .format(os.linesep))
|
||||
|
||||
return args.func(args, config, plugins)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main()) # pragma: no cover
|
||||
@@ -20,7 +20,6 @@ from letsencrypt.client import network2
|
||||
from letsencrypt.client import reverter
|
||||
from letsencrypt.client import revoker
|
||||
|
||||
from letsencrypt.client.plugins.apache import configurator
|
||||
from letsencrypt.client.display import ops as display_ops
|
||||
from letsencrypt.client.display import enhancements
|
||||
|
||||
@@ -154,7 +153,7 @@ class Client(object):
|
||||
:param str cert_path: Path to attempt to save the cert file
|
||||
:param str chain_path: Path to attempt to save the chain file
|
||||
|
||||
:returns: cert_file, chain_file (absolute paths to the actual files)
|
||||
:returns: cert_path, chain_path (absolute paths to the actual files)
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
:raises IOError: If unable to find room to write the cert files
|
||||
@@ -191,7 +190,7 @@ class Client(object):
|
||||
|
||||
return os.path.abspath(act_cert_path), cert_chain_abspath
|
||||
|
||||
def deploy_certificate(self, domains, privkey, cert_file, chain_file=None):
|
||||
def deploy_certificate(self, domains, privkey, cert_path, chain_path=None):
|
||||
"""Install certificate
|
||||
|
||||
:param list domains: list of domains to install the certificate
|
||||
@@ -199,8 +198,8 @@ class Client(object):
|
||||
:param privkey: private key for certificate
|
||||
:type privkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:param str cert_file: certificate file path
|
||||
:param str chain_file: chain file path
|
||||
:param str cert_path: certificate file path
|
||||
:param str chain_path: chain file path
|
||||
|
||||
"""
|
||||
if self.installer is None:
|
||||
@@ -208,13 +207,12 @@ class Client(object):
|
||||
"the certificate")
|
||||
raise errors.LetsEncryptClientError("No installer available")
|
||||
|
||||
chain = None if chain_file is None else os.path.abspath(chain_file)
|
||||
chain_path = None if chain_path is None else os.path.abspath(chain_path)
|
||||
|
||||
for dom in domains:
|
||||
self.installer.deploy_cert(dom,
|
||||
os.path.abspath(cert_file),
|
||||
os.path.abspath(privkey.file),
|
||||
chain)
|
||||
self.installer.deploy_cert(
|
||||
dom, os.path.abspath(cert_path),
|
||||
os.path.abspath(privkey.file), chain_path)
|
||||
|
||||
self.installer.save("Deployed Let's Encrypt Certificate")
|
||||
# sites may have been enabled / final cleanup
|
||||
@@ -313,74 +311,6 @@ def validate_key_csr(privkey, csr=None):
|
||||
"The key and CSR do not match")
|
||||
|
||||
|
||||
def list_available_authenticators(avail_auths):
|
||||
"""Return a pretty-printed list of authenticators.
|
||||
|
||||
This is used to provide helpful feedback in the case where a user
|
||||
specifies an invalid authenticator on the command line.
|
||||
|
||||
"""
|
||||
output_lines = ["Available authenticators:"]
|
||||
for auth_name, auth in avail_auths.iteritems():
|
||||
output_lines.append(" - %s : %s" % (auth_name, auth.description))
|
||||
return '\n'.join(output_lines)
|
||||
|
||||
|
||||
# This should be controlled by commandline parameters
|
||||
def determine_authenticator(all_auths, config):
|
||||
"""Returns a valid IAuthenticator.
|
||||
|
||||
:param list all_auths: Where each is a
|
||||
:class:`letsencrypt.client.interfaces.IAuthenticator` object
|
||||
|
||||
:param config: Used if an authenticator was specified on the command line.
|
||||
:type config: :class:`letsencrypt.client.interfaces.IConfig`
|
||||
|
||||
:returns: Valid Authenticator object or None
|
||||
|
||||
:raises letsencrypt.client.errors.LetsEncryptClientError: If no
|
||||
authenticator is available.
|
||||
|
||||
"""
|
||||
# Available Authenticator objects
|
||||
avail_auths = {}
|
||||
# Error messages for misconfigured authenticators
|
||||
errs = {}
|
||||
|
||||
for auth_name, auth in all_auths.iteritems():
|
||||
try:
|
||||
auth.prepare()
|
||||
except errors.LetsEncryptMisconfigurationError as err:
|
||||
errs[auth] = err
|
||||
except errors.LetsEncryptNoInstallationError:
|
||||
continue
|
||||
avail_auths[auth_name] = auth
|
||||
|
||||
# If an authenticator was specified on the command line, try to use it
|
||||
if config.authenticator:
|
||||
try:
|
||||
auth = avail_auths[config.authenticator]
|
||||
except KeyError:
|
||||
logging.info(list_available_authenticators(avail_auths))
|
||||
raise errors.LetsEncryptClientError(
|
||||
"The specified authenticator '%s' could not be found" %
|
||||
config.authenticator)
|
||||
elif len(avail_auths) > 1:
|
||||
auth = display_ops.choose_authenticator(avail_auths.values(), errs)
|
||||
elif len(avail_auths.keys()) == 1:
|
||||
auth = avail_auths[avail_auths.keys()[0]]
|
||||
else:
|
||||
raise errors.LetsEncryptClientError("No Authenticators available.")
|
||||
|
||||
if auth is not None and auth in errs:
|
||||
logging.error("Please fix the configuration for the Authenticator. "
|
||||
"The following error message was received: "
|
||||
"%s", errs[auth])
|
||||
return
|
||||
|
||||
return auth
|
||||
|
||||
|
||||
def determine_account(config):
|
||||
"""Determine which account to use.
|
||||
|
||||
@@ -403,29 +333,7 @@ def determine_account(config):
|
||||
return account.Account.from_prompts(config)
|
||||
|
||||
|
||||
def determine_installer(config):
|
||||
"""Returns a valid installer if one exists.
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`letsencrypt.client.interfaces.IConfig`
|
||||
|
||||
:returns: IInstaller or `None`
|
||||
:rtype: :class:`~letsencrypt.client.interfaces.IInstaller` or `None`
|
||||
|
||||
"""
|
||||
installer = configurator.ApacheConfigurator(config)
|
||||
try:
|
||||
installer.prepare()
|
||||
return installer
|
||||
except errors.LetsEncryptNoInstallationError:
|
||||
logging.info("Unable to find a way to install the certificate.")
|
||||
return
|
||||
except errors.LetsEncryptMisconfigurationError:
|
||||
# This will have to be changed in the future...
|
||||
return installer
|
||||
|
||||
|
||||
def rollback(checkpoints, config):
|
||||
def rollback(default_installer, checkpoints, config, plugins):
|
||||
"""Revert configuration the specified number of checkpoints.
|
||||
|
||||
:param int checkpoints: Number of checkpoints to revert.
|
||||
@@ -435,7 +343,9 @@ def rollback(checkpoints, config):
|
||||
|
||||
"""
|
||||
# Misconfigurations are only a slight problems... allow the user to rollback
|
||||
installer = determine_installer(config)
|
||||
installer = display_ops.pick_installer(
|
||||
config, default_installer, plugins, question="Which installer "
|
||||
"should be used for rollback?")
|
||||
|
||||
# No Errors occurred during init... proceed normally
|
||||
# If installer is None... couldn't find an installer... there shouldn't be
|
||||
@@ -445,18 +355,16 @@ def rollback(checkpoints, config):
|
||||
installer.restart()
|
||||
|
||||
|
||||
def revoke(config, no_confirm, cert, authkey):
|
||||
def revoke(default_installer, config, plugins, no_confirm, cert, authkey):
|
||||
"""Revoke certificates.
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`letsencrypt.client.interfaces.IConfig`
|
||||
|
||||
"""
|
||||
# Misconfigurations don't really matter. Determine installer better choose
|
||||
# correctly though.
|
||||
# This will need some better prepared or properly configured parameter...
|
||||
# I will figure it out later...
|
||||
installer = determine_installer(config)
|
||||
installer = display_ops.pick_installer(
|
||||
config, default_installer, plugins, question="Which installer "
|
||||
"should be used for certificate revocation?")
|
||||
|
||||
revoc = revoker.Revoker(installer, config, no_confirm)
|
||||
# Cert is most selective, so it is chosen first.
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
"""Let's Encrypt constants."""
|
||||
import pkg_resources
|
||||
import logging
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
|
||||
|
||||
SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins"
|
||||
"""Setuptools entry point group name for plugins."""
|
||||
|
||||
|
||||
CLI_DEFAULTS = dict(
|
||||
config_files=["/etc/letsencrypt/cli.ini"],
|
||||
verbose_count=-(logging.WARNING / 10),
|
||||
server="www.letsencrypt-demo.org/acme/new-reg",
|
||||
rsa_key_size=2048,
|
||||
rollback_checkpoints=0,
|
||||
config_dir="/etc/letsencrypt",
|
||||
work_dir="/var/lib/letsencrypt",
|
||||
backup_dir="/var/lib/letsencrypt/backups",
|
||||
key_dir="/etc/letsencrypt/keys",
|
||||
certs_dir="/etc/letsencrypt/certs",
|
||||
cert_path="/etc/letsencrypt/certs/cert-letsencrypt.pem",
|
||||
chain_path="/etc/letsencrypt/certs/chain-letsencrypt.pem",
|
||||
)
|
||||
"""Defaults for CLI flags and `.IConfig` attributes."""
|
||||
|
||||
|
||||
EXCLUSIVE_CHALLENGES = frozenset([frozenset([
|
||||
challenges.DVSNI, challenges.SimpleHTTPS])])
|
||||
"""Mutually exclusive challenges."""
|
||||
@@ -22,22 +43,6 @@ List of expected options parameters:
|
||||
"""
|
||||
|
||||
|
||||
APACHE_MOD_SSL_CONF = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.plugins.apache", "options-ssl.conf")
|
||||
"""Path to the Apache mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
|
||||
APACHE_REWRITE_HTTPS_ARGS = [
|
||||
"^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
|
||||
"""Apache rewrite rule arguments used for redirections to https vhost"""
|
||||
|
||||
|
||||
NGINX_MOD_SSL_CONF = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.plugins.nginx", "options-ssl.conf")
|
||||
"""Path to the Nginx mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
|
||||
|
||||
CONFIG_DIRS_MODE = 0o755
|
||||
"""Directory mode for ``.IConfig.config_dir`` et al."""
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Contains UI methods for LE user operations."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import zope.component
|
||||
@@ -6,41 +7,107 @@ import zope.component
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client.display import util as display_util
|
||||
|
||||
|
||||
# Define a helper function to avoid verbose code
|
||||
util = zope.component.getUtility # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def choose_authenticator(auths, errs):
|
||||
"""Allow the user to choose their authenticator.
|
||||
def choose_plugin(prepared, question):
|
||||
"""Allow the user to choose ther plugin.
|
||||
|
||||
:param list auths: Where each of type
|
||||
:class:`letsencrypt.client.interfaces.IAuthenticator` object
|
||||
:param dict errs: Mapping IAuthenticator objects to error messages
|
||||
:param list prepared: List of `~.PluginEntryPoint`.
|
||||
:param str question: Question to be presented to the user.
|
||||
|
||||
:returns: Authenticator selected
|
||||
:rtype: :class:`letsencrypt.client.interfaces.IAuthenticator` or `None`
|
||||
:returns: Plugin entry point chosen by the user.
|
||||
:rtype: `~.PluginEntryPoint`
|
||||
|
||||
"""
|
||||
descs = [auth.description if auth not in errs
|
||||
else "%s (Misconfigured)" % auth.description
|
||||
for auth in auths]
|
||||
opts = [plugin_ep.description_with_name
|
||||
+ (" [Misconfigured]" if plugin_ep.misconfigured else "")
|
||||
for plugin_ep in prepared]
|
||||
|
||||
while True:
|
||||
code, index = util(interfaces.IDisplay).menu(
|
||||
"How would you like to authenticate with the Let's Encrypt CA?",
|
||||
descs, help_label="More Info")
|
||||
question, opts, help_label="More Info")
|
||||
|
||||
if code == display_util.OK:
|
||||
return auths[index]
|
||||
return prepared[index]
|
||||
elif code == display_util.HELP:
|
||||
if auths[index] in errs:
|
||||
msg = "Reported Error: %s" % errs[auths[index]]
|
||||
if prepared[index].misconfigured:
|
||||
msg = "Reported Error: %s" % prepared[index].prepare()
|
||||
else:
|
||||
msg = auths[index].more_info()
|
||||
msg = prepared[index].init().more_info()
|
||||
util(interfaces.IDisplay).notification(
|
||||
msg, height=display_util.HEIGHT)
|
||||
else:
|
||||
return
|
||||
return None
|
||||
|
||||
|
||||
def pick_plugin(config, default, plugins, question, ifaces):
|
||||
"""Pick plugin.
|
||||
|
||||
:param letsencrypt.client.interfaces.IConfig: Configuration
|
||||
:param str default: Plugin name supplied by user or ``None``.
|
||||
:param letsencrypt.client.plugins.disco.PluginsRegistry plugins:
|
||||
All plugins registered as entry points.
|
||||
:param str question: Question to be presented to the user in case
|
||||
multiple candidates are found.
|
||||
:param list ifaces: Interfaces that plugins must provide.
|
||||
|
||||
:returns: Initialized plugin.
|
||||
:rtype: IPlugin
|
||||
|
||||
"""
|
||||
if default is not None:
|
||||
# throw more UX-friendly error if default not in plugins
|
||||
filtered = plugins.filter(lambda p_ep: p_ep.name == default)
|
||||
else:
|
||||
filtered = plugins.ifaces(ifaces)
|
||||
|
||||
filtered.init(config)
|
||||
verified = filtered.verify(ifaces)
|
||||
verified.prepare()
|
||||
prepared = verified.available()
|
||||
|
||||
if len(prepared) > 1:
|
||||
logging.debug("Multiple candidate plugins: %s", prepared)
|
||||
plugin_ep = choose_plugin(prepared.values(), question)
|
||||
if plugin_ep is None:
|
||||
return None
|
||||
else:
|
||||
return plugin_ep.init()
|
||||
elif len(prepared) == 1:
|
||||
plugin_ep = prepared.values()[0]
|
||||
logging.debug("Single candidate plugin: %s", plugin_ep)
|
||||
return plugin_ep.init()
|
||||
else:
|
||||
logging.debug("No candidate plugin")
|
||||
return None
|
||||
|
||||
|
||||
def pick_authenticator(
|
||||
config, default, plugins, question="How would you "
|
||||
"like to authenticate with Let's Encrypt CA?"):
|
||||
"""Pick authentication plugin."""
|
||||
return pick_plugin(
|
||||
config, default, plugins, question, (interfaces.IAuthenticator,))
|
||||
|
||||
|
||||
def pick_installer(config, default, plugins,
|
||||
question="How would you like to install certificates?"):
|
||||
"""Pick installer plugin."""
|
||||
return pick_plugin(
|
||||
config, default, plugins, question, (interfaces.IInstaller,))
|
||||
|
||||
|
||||
def pick_configurator(
|
||||
config, default, plugins,
|
||||
question="How would you like to authenticate and install "
|
||||
"certificates?"):
|
||||
"""Pick configurator plugin."""
|
||||
return pick_plugin(
|
||||
config, default, plugins, question,
|
||||
(interfaces.IAuthenticator, interfaces.IInstaller))
|
||||
|
||||
|
||||
def choose_account(accounts):
|
||||
@@ -76,6 +143,7 @@ def choose_names(installer):
|
||||
|
||||
"""
|
||||
if installer is None:
|
||||
logging.debug("No installer, picking names manually")
|
||||
return _choose_names_manually()
|
||||
|
||||
names = list(installer.get_all_names())
|
||||
|
||||
@@ -5,7 +5,88 @@ import zope.interface
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
class IAuthenticator(zope.interface.Interface):
|
||||
class IPluginFactory(zope.interface.Interface):
|
||||
"""IPlugin factory.
|
||||
|
||||
Objects providing this interface will be called without satisfying
|
||||
any entry point "extras" (extra dependencies) you might have defined
|
||||
for your plugin, e.g (excerpt from ``setup.py`` script)::
|
||||
|
||||
setup(
|
||||
...
|
||||
entry_points={
|
||||
'letsencrypt.plugins': [
|
||||
'name=example_project.plugin[plugin_deps]',
|
||||
],
|
||||
},
|
||||
extras_require={
|
||||
'plugin_deps': ['dep1', 'dep2'],
|
||||
}
|
||||
)
|
||||
|
||||
Therefore, make sure such objects are importable and usable without
|
||||
extras. This is necessary, because CLI does the following operations
|
||||
(in order):
|
||||
|
||||
- loads an entry point,
|
||||
- calls `inject_parser_options`,
|
||||
- requires an entry point,
|
||||
- creates plugin instance (`__call__`).
|
||||
|
||||
"""
|
||||
|
||||
description = zope.interface.Attribute("Short plugin description")
|
||||
|
||||
def __call__(config, name):
|
||||
"""Create new `IPlugin`.
|
||||
|
||||
:param IConfig config: Configuration.
|
||||
:param str name: Unique plugin name.
|
||||
|
||||
"""
|
||||
|
||||
def inject_parser_options(parser, name):
|
||||
"""Inject argument parser options (flags).
|
||||
|
||||
1. Be nice and prepend all options and destinations with
|
||||
`~.option_namespace` and `~.dest_namespace`.
|
||||
|
||||
2. Inject options (flags) only. Positional arguments are not
|
||||
allowed, as this would break the CLI.
|
||||
|
||||
:param ArgumentParser parser: (Almost) top-level CLI parser.
|
||||
:param str name: Unique plugin name.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class IPlugin(zope.interface.Interface):
|
||||
"""Let's Encrypt plugin."""
|
||||
|
||||
def prepare():
|
||||
"""Prepare the plugin.
|
||||
|
||||
Finish up any additional initialization.
|
||||
|
||||
:raises letsencrypt.client.errors.LetsEncryptMisconfigurationError:
|
||||
when full initialization cannot be completed. Plugin will be
|
||||
displayed on a list of available plugins.
|
||||
:raises letsencrypt.client.errors.LetsEncryptNoInstallationError:
|
||||
when the necessary programs/files cannot be located. Plugin
|
||||
will NOT be displayed on a list of available plugins.
|
||||
|
||||
"""
|
||||
|
||||
def more_info():
|
||||
"""Human-readable string to help the user.
|
||||
|
||||
Should describe the steps taken and any relevant info to help the user
|
||||
decide which plugin to use.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class IAuthenticator(IPlugin):
|
||||
"""Generic Let's Encrypt Authenticator.
|
||||
|
||||
Class represents all possible tools processes that have the
|
||||
@@ -13,22 +94,6 @@ class IAuthenticator(zope.interface.Interface):
|
||||
|
||||
"""
|
||||
|
||||
description = zope.interface.Attribute(
|
||||
"Short description of this authenticator. "
|
||||
"Used in interactive configuration.")
|
||||
|
||||
def prepare():
|
||||
"""Prepare the authenticator.
|
||||
|
||||
Finish up any additional initialization.
|
||||
|
||||
:raises letsencrypt.client.errors.LetsEncryptMisconfigurationError:
|
||||
when full initialization cannot be completed.
|
||||
:raises letsencrypt.client.errors.LetsEncryptNoInstallationError:
|
||||
when the necessary programs/files cannot be located.
|
||||
|
||||
"""
|
||||
|
||||
def get_chall_pref(domain):
|
||||
"""Return list of challenge preferences.
|
||||
|
||||
@@ -74,14 +139,6 @@ class IAuthenticator(zope.interface.Interface):
|
||||
|
||||
"""
|
||||
|
||||
def more_info():
|
||||
"""Human-readable string to help the user.
|
||||
|
||||
Should describe the steps taken and any relevant info to help the user
|
||||
decide which Authenticator to use.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class IConfig(zope.interface.Interface):
|
||||
"""Let's Encrypt user-supplied configuration.
|
||||
@@ -93,8 +150,6 @@ class IConfig(zope.interface.Interface):
|
||||
server = zope.interface.Attribute(
|
||||
"CA hostname (and optionally :port). The server certificate must "
|
||||
"be trusted in order to avoid further modifications to the client.")
|
||||
authenticator = zope.interface.Attribute(
|
||||
"Authenticator to use for responding to challenges.")
|
||||
email = zope.interface.Attribute(
|
||||
"Email used for registration and recovery contact.")
|
||||
rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
|
||||
@@ -120,49 +175,17 @@ class IConfig(zope.interface.Interface):
|
||||
|
||||
le_vhost_ext = zope.interface.Attribute(
|
||||
"SSL vhost configuration extension.")
|
||||
cert_path = zope.interface.Attribute("Let's Encrypt certificate file.")
|
||||
chain_path = zope.interface.Attribute("Let's Encrypt chain file.")
|
||||
|
||||
apache_server_root = zope.interface.Attribute(
|
||||
"Apache server root directory.")
|
||||
apache_ctl = zope.interface.Attribute(
|
||||
"Path to the 'apache2ctl' binary, used for 'configtest' and "
|
||||
"retrieving Apache2 version number.")
|
||||
apache_enmod = zope.interface.Attribute(
|
||||
"Path to the Apache 'a2enmod' binary.")
|
||||
apache_init_script = zope.interface.Attribute(
|
||||
"Path to the Apache init script (used for server reload/restart).")
|
||||
apache_mod_ssl_conf = zope.interface.Attribute(
|
||||
"Contains standard Apache SSL directives.")
|
||||
|
||||
nginx_server_root = zope.interface.Attribute(
|
||||
"Nginx server root directory.")
|
||||
nginx_ctl = zope.interface.Attribute(
|
||||
"Path to the 'nginx' binary, used for 'configtest' and "
|
||||
"retrieving nginx version number.")
|
||||
nginx_mod_ssl_conf = zope.interface.Attribute(
|
||||
"Contains standard nginx SSL directives.")
|
||||
cert_path = zope.interface.Attribute("Let's Encrypt certificate file path.")
|
||||
chain_path = zope.interface.Attribute("Let's Encrypt chain file path.")
|
||||
|
||||
|
||||
class IInstaller(zope.interface.Interface):
|
||||
class IInstaller(IPlugin):
|
||||
"""Generic Let's Encrypt Installer Interface.
|
||||
|
||||
Represents any server that an X509 certificate can be placed.
|
||||
|
||||
"""
|
||||
|
||||
def prepare():
|
||||
"""Prepare the installer.
|
||||
|
||||
Finish up any additional initialization.
|
||||
|
||||
:raises letsencrypt.client.errors.LetsEncryptMisconfigurationError`:
|
||||
when full initialization cannot be completed.
|
||||
:raises letsencrypt.errors.LetsEncryptNoInstallationError`:
|
||||
when the necessary programs/files cannot be located.
|
||||
|
||||
"""
|
||||
|
||||
def get_all_names():
|
||||
"""Returns all names that may be authenticated."""
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ from letsencrypt.acme import challenges
|
||||
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import augeas_configurator
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client import constants as core_constants
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
from letsencrypt.client.plugins.apache import constants
|
||||
from letsencrypt.client.plugins.apache import dvsni
|
||||
from letsencrypt.client.plugins.apache import obj
|
||||
from letsencrypt.client.plugins.apache import parser
|
||||
@@ -79,17 +80,34 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Apache Web Server"
|
||||
|
||||
def __init__(self, config, version=None):
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
|
||||
help="Apache server root directory.")
|
||||
add("mod-ssl-conf", default=constants.CLI_DEFAULTS["mod_ssl_conf"],
|
||||
help="Contains standard Apache SSL directives.")
|
||||
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
|
||||
help="Path to the 'apache2ctl' binary, used for 'configtest' and "
|
||||
"retrieving Apache2 version number.")
|
||||
add("enmod", default=constants.CLI_DEFAULTS["enmod"],
|
||||
help="Path to the Apache 'a2enmod' binary.")
|
||||
add("init-script", default=constants.CLI_DEFAULTS["init_script"],
|
||||
help="Path to the Apache init script (used for server "
|
||||
"reload/restart).")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize an Apache Configurator.
|
||||
|
||||
:param tup version: version of Apache as a tuple (2, 4, 7)
|
||||
(used mostly for unittesting)
|
||||
|
||||
"""
|
||||
super(ApacheConfigurator, self).__init__(config)
|
||||
version = kwargs.pop('version', None)
|
||||
super(ApacheConfigurator, self).__init__(*args, **kwargs)
|
||||
|
||||
# Verify that all directories and files exist with proper permissions
|
||||
if os.geteuid() == 0:
|
||||
@@ -109,8 +127,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
def prepare(self):
|
||||
"""Prepare the authenticator/installer."""
|
||||
self.parser = parser.ApacheParser(
|
||||
self.aug, self.config.apache_server_root,
|
||||
self.config.apache_mod_ssl_conf)
|
||||
self.aug, self.conf('server-root'), self.conf('mod-ssl-conf'))
|
||||
# Check for errors in parsing files with Augeas
|
||||
self.check_parsing_errors("httpd.aug")
|
||||
|
||||
@@ -128,9 +145,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# on initialization
|
||||
self._prepare_server_https()
|
||||
|
||||
temp_install(self.config.apache_mod_ssl_conf)
|
||||
temp_install(self.conf('mod-ssl-conf'))
|
||||
|
||||
def deploy_cert(self, domain, cert, key, cert_chain=None):
|
||||
def deploy_cert(self, domain, cert_path, key, chain_path=None):
|
||||
"""Deploys certificate to specified virtual host.
|
||||
|
||||
Currently tries to find the last directives to deploy the cert in
|
||||
@@ -146,25 +163,26 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
This shouldn't happen within letsencrypt though
|
||||
|
||||
:param str domain: domain to deploy certificate
|
||||
:param str cert: certificate filename
|
||||
:param str cert_path: certificate filename
|
||||
:param str key: private key filename
|
||||
:param str cert_chain: certificate chain filename
|
||||
:param str chain_path: certificate chain filename
|
||||
|
||||
"""
|
||||
vhost = self.choose_vhost(domain)
|
||||
# TODO(jdkasten): vhost might be None
|
||||
path = {}
|
||||
|
||||
path["cert_file"] = self.parser.find_dir(parser.case_i(
|
||||
path["cert_path"] = self.parser.find_dir(parser.case_i(
|
||||
"SSLCertificateFile"), None, vhost.path)
|
||||
path["cert_key"] = self.parser.find_dir(parser.case_i(
|
||||
"SSLCertificateKeyFile"), None, vhost.path)
|
||||
|
||||
# Only include if a certificate chain is specified
|
||||
if cert_chain is not None:
|
||||
path["cert_chain"] = self.parser.find_dir(
|
||||
if chain_path is not None:
|
||||
path["chain_path"] = self.parser.find_dir(
|
||||
parser.case_i("SSLCertificateChainFile"), None, vhost.path)
|
||||
|
||||
if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0:
|
||||
if not path["cert_path"] or not path["cert_key"]:
|
||||
# Throw some can't find all of the directives error"
|
||||
logging.warn(
|
||||
"Cannot find a cert or key directive in %s", vhost.path)
|
||||
@@ -174,22 +192,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
logging.info("Deploying Certificate to VirtualHost %s", vhost.filep)
|
||||
|
||||
self.aug.set(path["cert_file"][0], cert)
|
||||
self.aug.set(path["cert_path"][0], cert_path)
|
||||
self.aug.set(path["cert_key"][0], key)
|
||||
if cert_chain is not None:
|
||||
if len(path["cert_chain"]) == 0:
|
||||
if chain_path is not None:
|
||||
if not path["chain_path"]:
|
||||
self.parser.add_dir(
|
||||
vhost.path, "SSLCertificateChainFile", cert_chain)
|
||||
vhost.path, "SSLCertificateChainFile", chain_path)
|
||||
else:
|
||||
self.aug.set(path["cert_chain"][0], cert_chain)
|
||||
self.aug.set(path["chain_path"][0], chain_path)
|
||||
|
||||
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
|
||||
(vhost.filep,
|
||||
", ".join(str(addr) for addr in vhost.addrs)))
|
||||
self.save_notes += "\tSSLCertificateFile %s\n" % cert
|
||||
self.save_notes += "\tSSLCertificateFile %s\n" % cert_path
|
||||
self.save_notes += "\tSSLCertificateKeyFile %s\n" % key
|
||||
if cert_chain:
|
||||
self.save_notes += "\tSSLCertificateChainFile %s\n" % cert_chain
|
||||
if chain_path is not None:
|
||||
self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path
|
||||
|
||||
# Make sure vhost is enabled
|
||||
if not vhost.enabled:
|
||||
@@ -386,10 +404,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
is appropriately listening on port 443.
|
||||
|
||||
"""
|
||||
if not mod_loaded("ssl_module", self.config.apache_ctl):
|
||||
if not mod_loaded("ssl_module", self.conf('ctl')):
|
||||
logging.info("Loading mod_ssl into Apache Server")
|
||||
enable_mod("ssl", self.config.apache_init_script,
|
||||
self.config.apache_enmod)
|
||||
enable_mod("ssl", self.conf('init-script'),
|
||||
self.conf('enmod'))
|
||||
|
||||
# Check for Listen 443
|
||||
# Note: This could be made to also look for ip:443 combo
|
||||
@@ -572,9 +590,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`)
|
||||
|
||||
"""
|
||||
if not mod_loaded("rewrite_module", self.config.apache_ctl):
|
||||
enable_mod("rewrite", self.config.apache_init_script,
|
||||
self.config.apache_enmod)
|
||||
if not mod_loaded("rewrite_module", self.conf('ctl')):
|
||||
enable_mod("rewrite", self.conf('init-script'), self.conf('enmod'))
|
||||
|
||||
general_v = self._general_vhost(ssl_vhost)
|
||||
if general_v is None:
|
||||
@@ -599,7 +616,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# Add directives to server
|
||||
self.parser.add_dir(general_v.path, "RewriteEngine", "On")
|
||||
self.parser.add_dir(general_v.path, "RewriteRule",
|
||||
constants.APACHE_REWRITE_HTTPS_ARGS)
|
||||
constants.REWRITE_HTTPS_ARGS)
|
||||
self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" %
|
||||
(general_v.filep, ssl_vhost.filep))
|
||||
self.save()
|
||||
@@ -638,10 +655,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if not rewrite_path:
|
||||
# "No existing redirection for virtualhost"
|
||||
return False, -1
|
||||
if len(rewrite_path) == len(constants.APACHE_REWRITE_HTTPS_ARGS):
|
||||
if len(rewrite_path) == len(constants.REWRITE_HTTPS_ARGS):
|
||||
for idx, match in enumerate(rewrite_path):
|
||||
if (self.aug.get(match) !=
|
||||
constants.APACHE_REWRITE_HTTPS_ARGS[idx]):
|
||||
constants.REWRITE_HTTPS_ARGS[idx]):
|
||||
# Not a letsencrypt https rewrite
|
||||
return True, 2
|
||||
# Existing letsencrypt https rewrite rule is in place
|
||||
@@ -693,7 +710,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"LogLevel warn\n"
|
||||
"</VirtualHost>\n"
|
||||
% (servername, serveralias,
|
||||
" ".join(constants.APACHE_REWRITE_HTTPS_ARGS)))
|
||||
" ".join(constants.REWRITE_HTTPS_ARGS)))
|
||||
|
||||
# Write out the file
|
||||
# This is the default name
|
||||
@@ -897,7 +914,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return apache_restart(self.config.apache_init_script)
|
||||
return apache_restart(self.conf('init-script'))
|
||||
|
||||
def config_test(self): # pylint: disable=no-self-use
|
||||
"""Check the configuration of Apache for errors.
|
||||
@@ -908,7 +925,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[self.config.apache_ctl, "configtest"],
|
||||
[self.conf('ctl'), "configtest"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
@@ -935,11 +952,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
uid = os.geteuid()
|
||||
le_util.make_or_verify_dir(
|
||||
self.config.config_dir, constants.CONFIG_DIRS_MODE, uid)
|
||||
self.config.config_dir, core_constants.CONFIG_DIRS_MODE, uid)
|
||||
le_util.make_or_verify_dir(
|
||||
self.config.work_dir, constants.CONFIG_DIRS_MODE, uid)
|
||||
self.config.work_dir, core_constants.CONFIG_DIRS_MODE, uid)
|
||||
le_util.make_or_verify_dir(
|
||||
self.config.backup_dir, constants.CONFIG_DIRS_MODE, uid)
|
||||
self.config.backup_dir, core_constants.CONFIG_DIRS_MODE, uid)
|
||||
|
||||
def get_version(self):
|
||||
"""Return version of Apache Server.
|
||||
@@ -955,13 +972,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[self.config.apache_ctl, "-v"],
|
||||
[self.conf('ctl'), "-v"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
text = proc.communicate()[0]
|
||||
except (OSError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to run %s -v" % self.config.apache_ctl)
|
||||
"Unable to run %s -v" % self.conf('ctl'))
|
||||
|
||||
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
|
||||
matches = regex.findall(text)
|
||||
@@ -1165,4 +1182,4 @@ def temp_install(options_ssl):
|
||||
|
||||
# Check to make sure options-ssl.conf is installed
|
||||
if not os.path.isfile(options_ssl):
|
||||
shutil.copyfile(constants.APACHE_MOD_SSL_CONF, options_ssl)
|
||||
shutil.copyfile(constants.MOD_SSL_CONF, options_ssl)
|
||||
|
||||
22
letsencrypt/client/plugins/apache/constants.py
Normal file
22
letsencrypt/client/plugins/apache/constants.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Apache plugin constants."""
|
||||
import pkg_resources
|
||||
|
||||
|
||||
CLI_DEFAULTS = dict(
|
||||
server_root="/etc/apache2",
|
||||
mod_ssl_conf="/etc/letsencrypt/options-ssl.conf",
|
||||
ctl="apache2ctl",
|
||||
enmod="a2enmod",
|
||||
init_script="/etc/init.d/apache2",
|
||||
)
|
||||
"""CLI defaults."""
|
||||
|
||||
|
||||
MOD_SSL_CONF = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.plugins.apache", "options-ssl.conf")
|
||||
"""Path to the Apache mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
|
||||
REWRITE_HTTPS_ARGS = [
|
||||
"^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
|
||||
"""Apache rewrite rule arguments used for redirections to https vhost"""
|
||||
@@ -7,8 +7,8 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client.plugins.apache import configurator
|
||||
from letsencrypt.client.plugins.apache import constants
|
||||
from letsencrypt.client.plugins.apache import obj
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
|
||||
def setup_apache_ssl_options(config_dir):
|
||||
"""Move the ssl_options into position and return the path."""
|
||||
option_path = os.path.join(config_dir, "options-ssl.conf")
|
||||
shutil.copyfile(constants.APACHE_MOD_SSL_CONF, option_path)
|
||||
shutil.copyfile(constants.MOD_SSL_CONF, option_path)
|
||||
return option_path
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ def get_apache_configurator(
|
||||
# This just states that the ssl module is already loaded
|
||||
mock_popen().communicate.return_value = ("ssl_module", "")
|
||||
config = configurator.ApacheConfigurator(
|
||||
mock.MagicMock(
|
||||
config=mock.MagicMock(
|
||||
apache_server_root=config_path,
|
||||
apache_mod_ssl_conf=ssl_options,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
@@ -73,7 +73,8 @@ def get_apache_configurator(
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||
work_dir=work_dir),
|
||||
version)
|
||||
name="apache",
|
||||
version=version)
|
||||
|
||||
config.prepare()
|
||||
|
||||
|
||||
71
letsencrypt/client/plugins/common.py
Normal file
71
letsencrypt/client/plugins/common.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Plugin common functions."""
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.acme.jose import util as jose_util
|
||||
|
||||
from letsencrypt.client import interfaces
|
||||
|
||||
|
||||
def option_namespace(name):
|
||||
"""ArgumentParser options namespace (prefix of all options)."""
|
||||
return name + "-"
|
||||
|
||||
def dest_namespace(name):
|
||||
"""ArgumentParser dest namespace (prefix of all destinations)."""
|
||||
return name + "_"
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
"""Generic plugin."""
|
||||
zope.interface.implements(interfaces.IPlugin)
|
||||
# classProvides is not inherited, subclasses must define it on their own
|
||||
#zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
def __init__(self, config, name):
|
||||
self.config = config
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def option_namespace(self):
|
||||
"""ArgumentParser options namespace (prefix of all options)."""
|
||||
return option_namespace(self.name)
|
||||
|
||||
@property
|
||||
def dest_namespace(self):
|
||||
"""ArgumentParser dest namespace (prefix of all destinations)."""
|
||||
return dest_namespace(self.name)
|
||||
|
||||
def dest(self, var):
|
||||
"""Find a destination for given variable ``var``."""
|
||||
# this should do exactly the same what ArgumentParser(arg),
|
||||
# does to "arg" to compute "dest"
|
||||
return self.dest_namespace + var.replace("-", "_")
|
||||
|
||||
def conf(self, var):
|
||||
"""Find a configuration value for variable ``var``."""
|
||||
return getattr(self.config, self.dest(var))
|
||||
|
||||
@classmethod
|
||||
def inject_parser_options(cls, parser, name):
|
||||
"""Inject parser options.
|
||||
|
||||
See `~.IPlugin.inject_parser_options` for docs.
|
||||
|
||||
"""
|
||||
# dummy function, doesn't check if dest.startswith(self.dest_namespace)
|
||||
def add(arg_name_no_prefix, *args, **kwargs):
|
||||
# pylint: disable=missing-docstring
|
||||
return parser.add_argument(
|
||||
"--{0}{1}".format(option_namespace(name), arg_name_no_prefix),
|
||||
*args, **kwargs)
|
||||
return cls.add_parser_arguments(add)
|
||||
|
||||
@jose_util.abstractclassmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
"""Add plugin arguments to the CLI argument parser.
|
||||
|
||||
:param callable add: Function that proxies calls to
|
||||
`argparse.ArgumentParser.add_argument` prepending options
|
||||
with unique plugin name prefix.
|
||||
|
||||
"""
|
||||
61
letsencrypt/client/plugins/common_test.py
Normal file
61
letsencrypt/client/plugins/common_test.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Tests for letsencrypt.client.plugins.common."""
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
class NamespaceFunctionsTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.plugins.common.*_namespace functions."""
|
||||
|
||||
def test_option_namespace(self):
|
||||
from letsencrypt.client.plugins.common import option_namespace
|
||||
self.assertEqual("foo-", option_namespace("foo"))
|
||||
|
||||
def test_dest_namespace(self):
|
||||
from letsencrypt.client.plugins.common import dest_namespace
|
||||
self.assertEqual("foo_", dest_namespace("foo"))
|
||||
|
||||
|
||||
class PluginTest(unittest.TestCase):
|
||||
"""Test for letsencrypt.client.plugins.common.Plugin."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.common import Plugin
|
||||
|
||||
class MockPlugin(Plugin): # pylint: disable=missing-docstring
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("foo-bar", dest="different_to_foo_bar", x=1, y=None)
|
||||
|
||||
self.plugin_cls = MockPlugin
|
||||
self.config = mock.MagicMock()
|
||||
self.plugin = MockPlugin(config=self.config, name="mock")
|
||||
|
||||
def test_init(self):
|
||||
self.assertEqual("mock", self.plugin.name)
|
||||
self.assertEqual(self.config, self.plugin.config)
|
||||
|
||||
def test_option_namespace(self):
|
||||
self.assertEqual("mock-", self.plugin.option_namespace)
|
||||
|
||||
def test_dest_namespace(self):
|
||||
self.assertEqual("mock_", self.plugin.dest_namespace)
|
||||
|
||||
def test_dest(self):
|
||||
self.assertEqual("mock_foo_bar", self.plugin.dest("foo-bar"))
|
||||
self.assertEqual("mock_foo_bar", self.plugin.dest("foo_bar"))
|
||||
|
||||
def test_conf(self):
|
||||
self.assertEqual(self.config.mock_foo_bar, self.plugin.conf("foo-bar"))
|
||||
|
||||
def test_inject_parser_options(self):
|
||||
parser = mock.MagicMock()
|
||||
self.plugin_cls.inject_parser_options(parser, "mock")
|
||||
# note that inject_parser_options doesn't check if dest has
|
||||
# correct prefix
|
||||
parser.add_argument.assert_called_once_with(
|
||||
"--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
224
letsencrypt/client/plugins/disco.py
Normal file
224
letsencrypt/client/plugins/disco.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""Utilities for plugins discovery and selection."""
|
||||
import collections
|
||||
import logging
|
||||
import pkg_resources
|
||||
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
|
||||
|
||||
class PluginEntryPoint(object):
|
||||
"""Plugin entry point."""
|
||||
|
||||
PREFIX_FREE_DISTRIBUTIONS = ["letsencrypt"]
|
||||
"""Distributions for which prefix will be omitted."""
|
||||
|
||||
# this object is mutable, don't allow it to be hashed!
|
||||
__hash__ = None
|
||||
|
||||
def __init__(self, entry_point):
|
||||
self.name = self.entry_point_to_plugin_name(entry_point)
|
||||
self.plugin_cls = entry_point.load()
|
||||
self.entry_point = entry_point
|
||||
self._initialized = None
|
||||
self._prepared = None
|
||||
|
||||
@classmethod
|
||||
def entry_point_to_plugin_name(cls, entry_point):
|
||||
"""Unique plugin name for an ``entry_point``"""
|
||||
if entry_point.dist.key in cls.PREFIX_FREE_DISTRIBUTIONS:
|
||||
return entry_point.name
|
||||
return entry_point.dist.key + ":" + entry_point.name
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Description of the plugin."""
|
||||
return self.plugin_cls.description
|
||||
|
||||
@property
|
||||
def description_with_name(self):
|
||||
"""Description with name. Handy for UI."""
|
||||
return "{0} ({1})".format(self.description, self.name)
|
||||
|
||||
def ifaces(self, *ifaces_groups):
|
||||
"""Does plugin implements specified interface groups?"""
|
||||
return not ifaces_groups or any(
|
||||
all(iface.implementedBy(self.plugin_cls)
|
||||
for iface in ifaces)
|
||||
for ifaces in ifaces_groups)
|
||||
|
||||
@property
|
||||
def initialized(self):
|
||||
"""Has the plugin been initialized already?"""
|
||||
return self._initialized is not None
|
||||
|
||||
def init(self, config=None):
|
||||
"""Memoized plugin inititialization."""
|
||||
if not self.initialized:
|
||||
self.entry_point.require() # fetch extras!
|
||||
self._initialized = self.plugin_cls(config, self.name)
|
||||
return self._initialized
|
||||
|
||||
def verify(self, ifaces):
|
||||
"""Verify that the plugin conforms to the specified interfaces."""
|
||||
assert self.initialized
|
||||
for iface in ifaces: # zope.interface.providedBy(plugin)
|
||||
try:
|
||||
zope.interface.verify.verifyObject(iface, self.init())
|
||||
except zope.interface.exceptions.BrokenImplementation:
|
||||
if iface.implementedBy(self.plugin_cls):
|
||||
logging.debug(
|
||||
"%s implements %s but object does "
|
||||
"not verify", self.plugin_cls, iface.__name__)
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def prepared(self):
|
||||
"""Has the plugin been prepared already?"""
|
||||
if not self.initialized:
|
||||
logging.debug(".prepared called on uninitialized %r", self)
|
||||
return self._prepared is not None
|
||||
|
||||
def prepare(self):
|
||||
"""Memoized plugin preparation."""
|
||||
assert self.initialized
|
||||
if self._prepared is None:
|
||||
try:
|
||||
self._initialized.prepare()
|
||||
except errors.LetsEncryptMisconfigurationError as error:
|
||||
logging.debug("Misconfigured %r: %s", self, error)
|
||||
self._prepared = error
|
||||
except errors.LetsEncryptNoInstallationError as error:
|
||||
logging.debug("No installation (%r): %s", self, error)
|
||||
self._prepared = error
|
||||
else:
|
||||
self._prepared = True
|
||||
return self._prepared
|
||||
|
||||
@property
|
||||
def misconfigured(self):
|
||||
"""Is plugin misconfigured?"""
|
||||
return isinstance(
|
||||
self._prepared, errors.LetsEncryptMisconfigurationError)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Is plugin available, i.e. prepared or misconfigured?"""
|
||||
return self._prepared is True or self.misconfigured
|
||||
|
||||
def __repr__(self):
|
||||
return "PluginEntryPoint#{0}".format(self.name)
|
||||
|
||||
def __str__(self):
|
||||
lines = [
|
||||
"* {0}".format(self.name),
|
||||
"Description: {0}".format(self.plugin_cls.description),
|
||||
"Interfaces: {0}".format(", ".join(
|
||||
iface.__name__ for iface in zope.interface.implementedBy(
|
||||
self.plugin_cls))),
|
||||
"Entry point: {0}".format(self.entry_point),
|
||||
]
|
||||
|
||||
if self.initialized:
|
||||
lines.append("Initialized: {0}".format(self.init()))
|
||||
if self.prepared:
|
||||
lines.append("Prep: {0}".format(self.prepare()))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class PluginsRegistry(collections.Mapping):
|
||||
"""Plugins registry."""
|
||||
|
||||
def __init__(self, plugins):
|
||||
self._plugins = plugins
|
||||
|
||||
@classmethod
|
||||
def find_all(cls):
|
||||
"""Find plugins using setuptools entry points."""
|
||||
plugins = {}
|
||||
for entry_point in pkg_resources.iter_entry_points(
|
||||
constants.SETUPTOOLS_PLUGINS_ENTRY_POINT):
|
||||
plugin_ep = PluginEntryPoint(entry_point)
|
||||
assert plugin_ep.name not in plugins, (
|
||||
"PREFIX_FREE_DISTRIBUTIONS messed up")
|
||||
# providedBy | pylint: disable=no-member
|
||||
if interfaces.IPluginFactory.providedBy(plugin_ep.plugin_cls):
|
||||
plugins[plugin_ep.name] = plugin_ep
|
||||
else: # pragma: no cover
|
||||
logging.warning(
|
||||
"%r does not provide IPluginFactory, skipping", plugin_ep)
|
||||
return cls(plugins)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._plugins[name]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._plugins)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._plugins)
|
||||
|
||||
def init(self, config):
|
||||
"""Initialize all plugins in the registry."""
|
||||
return [plugin_ep.init(config) for plugin_ep
|
||||
in self._plugins.itervalues()]
|
||||
|
||||
def filter(self, pred):
|
||||
"""Filter plugins based on predicate."""
|
||||
return type(self)(dict((name, plugin_ep) for name, plugin_ep
|
||||
in self._plugins.iteritems() if pred(plugin_ep)))
|
||||
|
||||
def ifaces(self, *ifaces_groups):
|
||||
"""Filter plugins based on interfaces."""
|
||||
# pylint: disable=star-args
|
||||
return self.filter(lambda p_ep: p_ep.ifaces(*ifaces_groups))
|
||||
|
||||
def verify(self, ifaces):
|
||||
"""Filter plugins based on verification."""
|
||||
return self.filter(lambda p_ep: p_ep.verify(ifaces))
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare all plugins in the registry."""
|
||||
return [plugin_ep.prepare() for plugin_ep in self._plugins.itervalues()]
|
||||
|
||||
def available(self):
|
||||
"""Filter plugins based on availability."""
|
||||
return self.filter(lambda p_ep: p_ep.available)
|
||||
# succefully prepared + misconfigured
|
||||
|
||||
def find_init(self, plugin):
|
||||
"""Find an initialized plugin.
|
||||
|
||||
This is particularly useful for finding a name for the plugin
|
||||
(although `.IPluginFactory.__call__` takes ``name`` as one of
|
||||
the arguments, ``IPlugin.name`` is not part of the interface)::
|
||||
|
||||
# plugin is an instance providing IPlugin, initialized
|
||||
# somewhere else in the code
|
||||
plugin_registry.find_init(plugin).name
|
||||
|
||||
Returns ``None`` if ``plugin`` is not found in the registry.
|
||||
|
||||
"""
|
||||
# use list instead of set beacse PluginEntryPoint is not hashable
|
||||
candidates = [plugin_ep for plugin_ep in self._plugins.itervalues()
|
||||
if plugin_ep.initialized and plugin_ep.init() is plugin]
|
||||
assert len(candidates) <= 1
|
||||
if candidates:
|
||||
return candidates[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return "{0}({1!r})".format(
|
||||
self.__class__.__name__, set(self._plugins.itervalues()))
|
||||
|
||||
def __str__(self):
|
||||
if not self._plugins:
|
||||
return "No plugins"
|
||||
return "\n\n".join(str(p_ep) for p_ep in self._plugins.itervalues())
|
||||
243
letsencrypt/client/plugins/disco_test.py
Normal file
243
letsencrypt/client/plugins/disco_test.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""Tests for letsencrypt.client.plugins.disco."""
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
|
||||
from letsencrypt.client.plugins.standalone import authenticator
|
||||
|
||||
EP_SA = pkg_resources.EntryPoint(
|
||||
"sa", "letsencrypt.client.plugins.standalone.authenticator",
|
||||
attrs=("StandaloneAuthenticator",),
|
||||
dist=mock.MagicMock(key="letsencrypt"))
|
||||
|
||||
|
||||
class PluginEntryPointTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.plugins.disco.PluginEntryPoint."""
|
||||
|
||||
def setUp(self):
|
||||
self.ep1 = pkg_resources.EntryPoint(
|
||||
"ep1", "p1.ep1", dist=mock.MagicMock(key="p1"))
|
||||
self.ep1prim = pkg_resources.EntryPoint(
|
||||
"ep1", "p2.ep2", dist=mock.MagicMock(key="p2"))
|
||||
# nested
|
||||
self.ep2 = pkg_resources.EntryPoint(
|
||||
"ep2", "p2.foo.ep2", dist=mock.MagicMock(key="p2"))
|
||||
# project name != top-level package name
|
||||
self.ep3 = pkg_resources.EntryPoint(
|
||||
"ep3", "a.ep3", dist=mock.MagicMock(key="p3"))
|
||||
|
||||
from letsencrypt.client.plugins.disco import PluginEntryPoint
|
||||
self.plugin_ep = PluginEntryPoint(EP_SA)
|
||||
|
||||
def test_entry_point_to_plugin_name(self):
|
||||
from letsencrypt.client.plugins.disco import PluginEntryPoint
|
||||
|
||||
names = {
|
||||
self.ep1: "p1:ep1",
|
||||
self.ep1prim: "p2:ep1",
|
||||
self.ep2: "p2:ep2",
|
||||
self.ep3: "p3:ep3",
|
||||
EP_SA: "sa",
|
||||
}
|
||||
|
||||
for entry_point, name in names.iteritems():
|
||||
self.assertEqual(
|
||||
name, PluginEntryPoint.entry_point_to_plugin_name(entry_point))
|
||||
|
||||
def test_description(self):
|
||||
self.assertEqual("Standalone Authenticator", self.plugin_ep.description)
|
||||
|
||||
def test_description_with_name(self):
|
||||
self.plugin_ep.plugin_cls = mock.MagicMock(description="Desc")
|
||||
self.assertEqual(
|
||||
"Desc (sa)", self.plugin_ep.description_with_name)
|
||||
|
||||
def test_ifaces(self):
|
||||
self.assertTrue(self.plugin_ep.ifaces((interfaces.IAuthenticator,)))
|
||||
self.assertFalse(self.plugin_ep.ifaces((interfaces.IInstaller,)))
|
||||
self.assertFalse(self.plugin_ep.ifaces((
|
||||
interfaces.IInstaller, interfaces.IAuthenticator)))
|
||||
|
||||
def test__init__(self):
|
||||
self.assertFalse(self.plugin_ep.initialized)
|
||||
self.assertFalse(self.plugin_ep.prepared)
|
||||
self.assertFalse(self.plugin_ep.misconfigured)
|
||||
self.assertFalse(self.plugin_ep.available)
|
||||
self.assertTrue(self.plugin_ep.entry_point is EP_SA)
|
||||
self.assertEqual("sa", self.plugin_ep.name)
|
||||
|
||||
self.assertTrue(
|
||||
self.plugin_ep.plugin_cls is authenticator.StandaloneAuthenticator)
|
||||
|
||||
def test_init(self):
|
||||
config = mock.MagicMock()
|
||||
plugin = self.plugin_ep.init(config=config)
|
||||
self.assertTrue(self.plugin_ep.initialized)
|
||||
self.assertTrue(plugin.config is config)
|
||||
# memoize!
|
||||
self.assertTrue(self.plugin_ep.init() is plugin)
|
||||
self.assertTrue(plugin.config is config)
|
||||
# try to give different config
|
||||
self.assertTrue(self.plugin_ep.init(123) is plugin)
|
||||
self.assertTrue(plugin.config is config)
|
||||
|
||||
self.assertFalse(self.plugin_ep.prepared)
|
||||
self.assertFalse(self.plugin_ep.misconfigured)
|
||||
self.assertFalse(self.plugin_ep.available)
|
||||
|
||||
def test_verify(self):
|
||||
iface1 = mock.MagicMock(__name__="iface1")
|
||||
iface2 = mock.MagicMock(__name__="iface2")
|
||||
iface3 = mock.MagicMock(__name__="iface3")
|
||||
# pylint: disable=protected-access
|
||||
self.plugin_ep._initialized = plugin = mock.MagicMock()
|
||||
|
||||
exceptions = zope.interface.exceptions
|
||||
with mock.patch("letsencrypt.client.plugins."
|
||||
"disco.zope.interface") as mock_zope:
|
||||
mock_zope.exceptions = exceptions
|
||||
def verify_object(iface, obj): # pylint: disable=missing-docstring
|
||||
assert obj is plugin
|
||||
assert iface is iface1 or iface is iface2 or iface is iface3
|
||||
if iface is iface3:
|
||||
raise mock_zope.exceptions.BrokenImplementation(None, None)
|
||||
mock_zope.verify.verifyObject.side_effect = verify_object
|
||||
self.assertTrue(self.plugin_ep.verify((iface1,)))
|
||||
self.assertTrue(self.plugin_ep.verify((iface1, iface2)))
|
||||
self.assertFalse(self.plugin_ep.verify((iface3,)))
|
||||
self.assertFalse(self.plugin_ep.verify((iface1, iface3)))
|
||||
|
||||
def test_prepare(self):
|
||||
config = mock.MagicMock()
|
||||
self.plugin_ep.init(config=config)
|
||||
self.plugin_ep.prepare()
|
||||
self.assertTrue(self.plugin_ep.prepared)
|
||||
self.assertFalse(self.plugin_ep.misconfigured)
|
||||
|
||||
# output doesn't matter that much, just test if it runs
|
||||
str(self.plugin_ep)
|
||||
|
||||
def test_prepare_misconfigured(self):
|
||||
plugin = mock.MagicMock()
|
||||
plugin.prepare.side_effect = errors.LetsEncryptMisconfigurationError
|
||||
# pylint: disable=protected-access
|
||||
self.plugin_ep._initialized = plugin
|
||||
self.assertTrue(isinstance(self.plugin_ep.prepare(),
|
||||
errors.LetsEncryptMisconfigurationError))
|
||||
self.assertTrue(self.plugin_ep.prepared)
|
||||
self.assertTrue(self.plugin_ep.misconfigured)
|
||||
self.assertTrue(self.plugin_ep.available)
|
||||
|
||||
def test_prepare_no_installation(self):
|
||||
plugin = mock.MagicMock()
|
||||
plugin.prepare.side_effect = errors.LetsEncryptNoInstallationError
|
||||
# pylint: disable=protected-access
|
||||
self.plugin_ep._initialized = plugin
|
||||
self.assertTrue(isinstance(self.plugin_ep.prepare(),
|
||||
errors.LetsEncryptNoInstallationError))
|
||||
self.assertTrue(self.plugin_ep.prepared)
|
||||
self.assertFalse(self.plugin_ep.misconfigured)
|
||||
self.assertFalse(self.plugin_ep.available)
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual("PluginEntryPoint#sa", repr(self.plugin_ep))
|
||||
|
||||
|
||||
class PluginsRegistryTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.plugins.disco.PluginsRegistry."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.disco import PluginsRegistry
|
||||
self.plugin_ep = mock.MagicMock(name="mock")
|
||||
self.plugins = {"mock": self.plugin_ep}
|
||||
self.reg = PluginsRegistry(self.plugins)
|
||||
|
||||
def test_find_all(self):
|
||||
from letsencrypt.client.plugins.disco import PluginsRegistry
|
||||
with mock.patch("letsencrypt.client.plugins.disco"
|
||||
".pkg_resources") as mock_pkg:
|
||||
mock_pkg.iter_entry_points.return_value = iter([EP_SA])
|
||||
plugins = PluginsRegistry.find_all()
|
||||
self.assertTrue(plugins["sa"].plugin_cls
|
||||
is authenticator.StandaloneAuthenticator)
|
||||
self.assertTrue(plugins["sa"].entry_point is EP_SA)
|
||||
|
||||
def test_getitem(self):
|
||||
self.assertEqual(self.plugin_ep, self.reg["mock"])
|
||||
|
||||
def test_iter(self):
|
||||
self.assertEqual(["mock"], list(self.reg))
|
||||
|
||||
def test_len(self):
|
||||
self.assertEqual(1, len(self.reg))
|
||||
self.plugins.clear()
|
||||
self.assertEqual(0, len(self.reg))
|
||||
|
||||
def test_init(self):
|
||||
self.plugin_ep.init.return_value = "baz"
|
||||
self.assertEqual(["baz"], self.reg.init("bar"))
|
||||
self.plugin_ep.init.assert_called_once_with("bar")
|
||||
|
||||
def test_filter(self):
|
||||
self.plugins.update({
|
||||
"foo": "bar",
|
||||
"bar": "foo",
|
||||
"baz": "boo",
|
||||
})
|
||||
self.assertEqual(
|
||||
{"foo": "bar", "baz": "boo"},
|
||||
self.reg.filter(lambda p_ep: str(p_ep).startswith("b")))
|
||||
|
||||
def test_ifaces(self):
|
||||
self.plugin_ep.ifaces.return_value = True
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.plugins, self.reg.ifaces()._plugins)
|
||||
self.plugin_ep.ifaces.return_value = False
|
||||
self.assertEqual({}, self.reg.ifaces()._plugins)
|
||||
|
||||
def test_verify(self):
|
||||
self.plugin_ep.verify.return_value = True
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(
|
||||
self.plugins, self.reg.verify(mock.MagicMock())._plugins)
|
||||
self.plugin_ep.verify.return_value = False
|
||||
self.assertEqual({}, self.reg.verify(mock.MagicMock())._plugins)
|
||||
|
||||
def test_prepare(self):
|
||||
self.plugin_ep.prepare.return_value = "baz"
|
||||
self.assertEqual(["baz"], self.reg.prepare())
|
||||
self.plugin_ep.prepare.assert_called_once_with()
|
||||
|
||||
def test_available(self):
|
||||
self.plugin_ep.available = True
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.plugins, self.reg.available()._plugins)
|
||||
self.plugin_ep.available = False
|
||||
self.assertEqual({}, self.reg.available()._plugins)
|
||||
|
||||
def test_find_init(self):
|
||||
self.assertTrue(self.reg.find_init(mock.Mock()) is None)
|
||||
self.plugin_ep.initalized = True
|
||||
self.assertTrue(
|
||||
self.reg.find_init(self.plugin_ep.init()) is self.plugin_ep)
|
||||
|
||||
def test_repr(self):
|
||||
self.plugin_ep.__repr__ = lambda _: "PluginEntryPoint#mock"
|
||||
self.assertEqual("PluginsRegistry(set([PluginEntryPoint#mock]))",
|
||||
repr(self.reg))
|
||||
|
||||
def test_str(self):
|
||||
self.plugin_ep.__str__ = lambda _: "Mock"
|
||||
self.plugins["foo"] = "Mock"
|
||||
self.assertEqual("Mock\n\nMock", str(self.reg))
|
||||
self.plugins.clear()
|
||||
self.assertEqual("No plugins", str(self.reg))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,17 +12,20 @@ import zope.interface
|
||||
from letsencrypt.acme import challenges
|
||||
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client import constants as core_constants
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import reverter
|
||||
|
||||
from letsencrypt.client.plugins import common
|
||||
|
||||
from letsencrypt.client.plugins.nginx import constants
|
||||
from letsencrypt.client.plugins.nginx import dvsni
|
||||
from letsencrypt.client.plugins.nginx import parser
|
||||
|
||||
|
||||
class NginxConfigurator(object):
|
||||
class NginxConfigurator(common.Plugin):
|
||||
# pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||
"""Nginx configurator.
|
||||
|
||||
@@ -46,17 +49,29 @@ class NginxConfigurator(object):
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Nginx Web Server"
|
||||
|
||||
def __init__(self, config, version=None):
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
|
||||
help="Nginx server root directory.")
|
||||
add("mod-ssl-conf", default=constants.CLI_DEFAULTS["mod_ssl_conf"],
|
||||
help="Contains standard nginx SSL directives.")
|
||||
add("ctl", default=constants.CLI_DEFAULTS["ctl"], help="Path to the "
|
||||
"'nginx' binary, used for 'configtest' and retrieving nginx "
|
||||
"version number.")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize an Nginx Configurator.
|
||||
|
||||
:param tup version: version of Nginx as a tuple (1, 4, 7)
|
||||
(used mostly for unittesting)
|
||||
|
||||
"""
|
||||
self.config = config
|
||||
version = kwargs.pop("version", None)
|
||||
super(NginxConfigurator, self).__init__(*args, **kwargs)
|
||||
|
||||
# Verify that all directories and files exist with proper permissions
|
||||
if os.geteuid() == 0:
|
||||
@@ -74,21 +89,21 @@ class NginxConfigurator(object):
|
||||
self._enhance_func = {} # TODO: Support at least redirects
|
||||
|
||||
# Set up reverter
|
||||
self.reverter = reverter.Reverter(config)
|
||||
self.reverter = reverter.Reverter(self.config)
|
||||
self.reverter.recovery_routine()
|
||||
|
||||
# This is called in determine_authenticator and determine_installer
|
||||
def prepare(self):
|
||||
"""Prepare the authenticator/installer."""
|
||||
self.parser = parser.NginxParser(
|
||||
self.config.nginx_server_root,
|
||||
self.config.nginx_mod_ssl_conf)
|
||||
self.conf('server-root'),
|
||||
self.conf('mod-ssl-conf'))
|
||||
|
||||
# Set Version
|
||||
if self.version is None:
|
||||
self.version = self.get_version()
|
||||
|
||||
temp_install(self.config.nginx_mod_ssl_conf)
|
||||
temp_install(self.conf('mod-ssl-conf'))
|
||||
|
||||
# Entry point in main.py for installing cert
|
||||
def deploy_cert(self, domain, cert, key, cert_chain=None):
|
||||
@@ -312,7 +327,7 @@ class NginxConfigurator(object):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return nginx_restart(self.config.nginx_ctl)
|
||||
return nginx_restart(self.conf('ctl'))
|
||||
|
||||
def config_test(self): # pylint: disable=no-self-use
|
||||
"""Check the configuration of Nginx for errors.
|
||||
@@ -323,7 +338,7 @@ class NginxConfigurator(object):
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[self.config.nginx_ctl, "-t"],
|
||||
[self.conf('ctl'), "-t"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
@@ -350,11 +365,11 @@ class NginxConfigurator(object):
|
||||
"""
|
||||
uid = os.geteuid()
|
||||
le_util.make_or_verify_dir(
|
||||
self.config.work_dir, constants.CONFIG_DIRS_MODE, uid)
|
||||
self.config.work_dir, core_constants.CONFIG_DIRS_MODE, uid)
|
||||
le_util.make_or_verify_dir(
|
||||
self.config.backup_dir, constants.CONFIG_DIRS_MODE, uid)
|
||||
self.config.backup_dir, core_constants.CONFIG_DIRS_MODE, uid)
|
||||
le_util.make_or_verify_dir(
|
||||
self.config.config_dir, constants.CONFIG_DIRS_MODE, uid)
|
||||
self.config.config_dir, core_constants.CONFIG_DIRS_MODE, uid)
|
||||
|
||||
def get_version(self):
|
||||
"""Return version of Nginx Server.
|
||||
@@ -370,13 +385,13 @@ class NginxConfigurator(object):
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[self.config.nginx_ctl, "-V"],
|
||||
[self.conf('ctl'), "-V"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
text = proc.communicate()[1] # nginx prints output to stderr
|
||||
except (OSError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to run %s -V" % self.config.nginx_ctl)
|
||||
"Unable to run %s -V" % self.conf('ctl'))
|
||||
|
||||
version_regex = re.compile(r"nginx/([0-9\.]*)", re.IGNORECASE)
|
||||
version_matches = version_regex.findall(text)
|
||||
@@ -561,4 +576,4 @@ def temp_install(options_ssl):
|
||||
|
||||
# Check to make sure options-ssl.conf is installed
|
||||
if not os.path.isfile(options_ssl):
|
||||
shutil.copyfile(constants.NGINX_MOD_SSL_CONF, options_ssl)
|
||||
shutil.copyfile(constants.MOD_SSL_CONF, options_ssl)
|
||||
|
||||
16
letsencrypt/client/plugins/nginx/constants.py
Normal file
16
letsencrypt/client/plugins/nginx/constants.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""nginx plugin constants."""
|
||||
import pkg_resources
|
||||
|
||||
|
||||
CLI_DEFAULTS = dict(
|
||||
server_root="/etc/nginx",
|
||||
mod_ssl_conf="/etc/letsencrypt/options-ssl-nginx.conf",
|
||||
ctl="nginx",
|
||||
)
|
||||
"""CLI defaults."""
|
||||
|
||||
|
||||
MOD_SSL_CONF = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.plugins.nginx", "options-ssl.conf")
|
||||
"""Path to the Nginx mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client.plugins.nginx import constants
|
||||
from letsencrypt.client.plugins.nginx import configurator
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ def dir_setup(test_dir="debian_nginx/two_vhost_80"):
|
||||
def setup_nginx_ssl_options(config_dir):
|
||||
"""Move the ssl_options into position and return the path."""
|
||||
option_path = os.path.join(config_dir, "options-ssl.conf")
|
||||
shutil.copyfile(constants.NGINX_MOD_SSL_CONF, option_path)
|
||||
shutil.copyfile(constants.MOD_SSL_CONF, option_path)
|
||||
return option_path
|
||||
|
||||
|
||||
@@ -65,12 +65,13 @@ def get_nginx_configurator(
|
||||
backups = os.path.join(work_dir, "backups")
|
||||
|
||||
config = configurator.NginxConfigurator(
|
||||
mock.MagicMock(
|
||||
config=mock.MagicMock(
|
||||
nginx_server_root=config_path, nginx_mod_ssl_conf=ssl_options,
|
||||
le_vhost_ext="-le-ssl.conf", backup_dir=backups,
|
||||
config_dir=config_dir, work_dir=work_dir,
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS")),
|
||||
version)
|
||||
name="nginx",
|
||||
version=version)
|
||||
config.prepare()
|
||||
return config
|
||||
|
||||
@@ -17,8 +17,10 @@ from letsencrypt.acme import challenges
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import interfaces
|
||||
|
||||
from letsencrypt.client.plugins import common
|
||||
|
||||
class StandaloneAuthenticator(object):
|
||||
|
||||
class StandaloneAuthenticator(common.Plugin):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
"""Standalone authenticator.
|
||||
|
||||
@@ -29,10 +31,12 @@ class StandaloneAuthenticator(object):
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Standalone Authenticator"
|
||||
|
||||
def __init__(self, unused_config):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StandaloneAuthenticator, self).__init__(*args, **kwargs)
|
||||
self.child_pid = None
|
||||
self.parent_pid = os.getpid()
|
||||
self.subproc_state = None
|
||||
|
||||
@@ -59,7 +59,7 @@ class ChallPrefTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
|
||||
def test_chall_pref(self):
|
||||
self.assertEqual(self.authenticator.get_chall_pref("example.com"),
|
||||
@@ -71,7 +71,7 @@ class SNICallbackTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
self.cert = achallenges.DVSNI(
|
||||
challb=acme_util.DVSNI_P,
|
||||
domain="example.com", key=KEY).gen_cert_and_response()[0]
|
||||
@@ -109,7 +109,7 @@ class ClientSignalHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
self.authenticator.tasks = {"foononce.acme.invalid": "stuff"}
|
||||
self.authenticator.child_pid = 12345
|
||||
|
||||
@@ -138,7 +138,7 @@ class SubprocSignalHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
self.authenticator.tasks = {"foononce.acme.invalid": "stuff"}
|
||||
self.authenticator.child_pid = 12345
|
||||
self.authenticator.parent_pid = 23456
|
||||
@@ -190,7 +190,7 @@ class AlreadyListeningTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
|
||||
"net_connections")
|
||||
@@ -297,7 +297,7 @@ class PerformTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
|
||||
self.achall1 = achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
@@ -371,7 +371,7 @@ class StartListenerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"Crypto.Random.atfork")
|
||||
@@ -406,7 +406,7 @@ class DoParentProcessTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"signal.signal")
|
||||
@@ -460,7 +460,7 @@ class DoChildProcessTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
self.cert = achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(r=("x" * 32), nonce="abcdef"), "pending"),
|
||||
@@ -549,7 +549,7 @@ class CleanupTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
self.achall = achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(r="whee", nonce="foononce"), "pending"),
|
||||
@@ -582,7 +582,7 @@ class MoreInfoTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import (
|
||||
StandaloneAuthenticator)
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
|
||||
def test_more_info(self):
|
||||
"""Make sure exceptions aren't raised."""
|
||||
@@ -594,7 +594,7 @@ class InitTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.standalone.authenticator import (
|
||||
StandaloneAuthenticator)
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator = StandaloneAuthenticator(config=None, name=None)
|
||||
|
||||
def test_prepare(self):
|
||||
"""Make sure exceptions aren't raised.
|
||||
|
||||
35
letsencrypt/client/tests/cli_test.py
Normal file
35
letsencrypt/client/tests/cli_test.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Tests for letsencrypt.client.cli."""
|
||||
import itertools
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
class CLITest(unittest.TestCase):
|
||||
"""Tests for different commands."""
|
||||
|
||||
@classmethod
|
||||
def _call(cls, args):
|
||||
from letsencrypt.client import cli
|
||||
args = ['--text'] + args
|
||||
with mock.patch("letsencrypt.client.cli.sys.stdout") as stdout:
|
||||
with mock.patch("letsencrypt.client.cli.sys.stderr") as stderr:
|
||||
ret = cli.main(args)
|
||||
return ret, stdout, stderr
|
||||
|
||||
def test_no_flags(self):
|
||||
self.assertRaises(SystemExit, self._call, [])
|
||||
|
||||
def test_help(self):
|
||||
self.assertRaises(SystemExit, self._call, ['--help'])
|
||||
|
||||
def test_plugins(self):
|
||||
flags = ['--init', '--prepare', '--authenticators', '--installers']
|
||||
for args in itertools.chain(
|
||||
*(itertools.combinations(flags, r)
|
||||
for r in xrange(len(flags)))):
|
||||
self._call(['plugins',] + list(args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -8,7 +8,6 @@ import mock
|
||||
|
||||
from letsencrypt.client import account
|
||||
from letsencrypt.client import configuration
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
|
||||
@@ -54,92 +53,6 @@ class DetermineAccountTest(unittest.TestCase):
|
||||
self.assertTrue(chosen_acc.email, test_acc.email)
|
||||
|
||||
|
||||
class DetermineAuthenticatorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.plugins.apache.configurator import (
|
||||
ApacheConfigurator)
|
||||
from letsencrypt.client.plugins.standalone.authenticator import (
|
||||
StandaloneAuthenticator)
|
||||
|
||||
self.mock_stand = mock.MagicMock(
|
||||
spec=StandaloneAuthenticator, description="Apache Web Server")
|
||||
self.mock_apache = mock.MagicMock(
|
||||
spec=ApacheConfigurator, description="Standalone Authenticator")
|
||||
|
||||
self.mock_config = mock.MagicMock(
|
||||
spec=configuration.NamespaceConfig, authenticator=None)
|
||||
|
||||
self.all_auths = {
|
||||
'apache': self.mock_apache,
|
||||
'standalone': self.mock_stand
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _call(cls, all_auths, config):
|
||||
from letsencrypt.client.client import determine_authenticator
|
||||
return determine_authenticator(all_auths, config)
|
||||
|
||||
@mock.patch("letsencrypt.client.client.display_ops.choose_authenticator")
|
||||
def test_accept_two(self, mock_choose):
|
||||
mock_choose.return_value = self.mock_stand()
|
||||
self.assertEqual(self._call(self.all_auths, self.mock_config),
|
||||
self.mock_stand())
|
||||
|
||||
def test_accept_one(self):
|
||||
self.mock_apache.prepare.return_value = self.mock_apache
|
||||
one_avail_auth = {
|
||||
'apache': self.mock_apache
|
||||
}
|
||||
self.assertEqual(self._call(one_avail_auth, self.mock_config),
|
||||
self.mock_apache)
|
||||
|
||||
def test_no_installation_one(self):
|
||||
self.mock_apache.prepare.side_effect = (
|
||||
errors.LetsEncryptNoInstallationError)
|
||||
|
||||
self.assertEqual(self._call(self.all_auths, self.mock_config),
|
||||
self.mock_stand)
|
||||
|
||||
def test_no_installations(self):
|
||||
self.mock_apache.prepare.side_effect = (
|
||||
errors.LetsEncryptNoInstallationError)
|
||||
self.mock_stand.prepare.side_effect = (
|
||||
errors.LetsEncryptNoInstallationError)
|
||||
|
||||
self.assertRaises(errors.LetsEncryptClientError,
|
||||
self._call,
|
||||
self.all_auths,
|
||||
self.mock_config)
|
||||
|
||||
@mock.patch("letsencrypt.client.client.logging")
|
||||
@mock.patch("letsencrypt.client.client.display_ops.choose_authenticator")
|
||||
def test_misconfigured(self, mock_choose, unused_log):
|
||||
self.mock_apache.prepare.side_effect = (
|
||||
errors.LetsEncryptMisconfigurationError)
|
||||
mock_choose.return_value = self.mock_apache
|
||||
|
||||
self.assertTrue(self._call(self.all_auths, self.mock_config) is None)
|
||||
|
||||
def test_choose_valid_auth_from_cmd_line(self):
|
||||
standalone_config = mock.MagicMock(spec=configuration.NamespaceConfig,
|
||||
authenticator='standalone')
|
||||
self.assertEqual(self._call(self.all_auths, standalone_config),
|
||||
self.mock_stand)
|
||||
|
||||
apache_config = mock.MagicMock(spec=configuration.NamespaceConfig,
|
||||
authenticator='apache')
|
||||
self.assertEqual(self._call(self.all_auths, apache_config),
|
||||
self.mock_apache)
|
||||
|
||||
def test_choose_invalid_auth_from_cmd_line(self):
|
||||
invalid_config = mock.MagicMock(spec=configuration.NamespaceConfig,
|
||||
authenticator='foobar')
|
||||
self.assertRaises(errors.LetsEncryptClientError,
|
||||
self._call,
|
||||
self.all_auths,
|
||||
invalid_config)
|
||||
|
||||
|
||||
class RollbackTest(unittest.TestCase):
|
||||
"""Test the rollback function."""
|
||||
def setUp(self):
|
||||
@@ -150,23 +63,22 @@ class RollbackTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def _call(cls, checkpoints):
|
||||
from letsencrypt.client.client import rollback
|
||||
rollback(checkpoints, mock.MagicMock())
|
||||
rollback(None, checkpoints, {}, mock.MagicMock())
|
||||
|
||||
@mock.patch("letsencrypt.client.client.determine_installer")
|
||||
def test_no_problems(self, mock_det):
|
||||
mock_det.side_effect = self.m_install
|
||||
@mock.patch("letsencrypt.client.client.display_ops.pick_installer")
|
||||
def test_no_problems(self, mock_pick_installer):
|
||||
mock_pick_installer.side_effect = self.m_install
|
||||
|
||||
self._call(1)
|
||||
|
||||
self.assertEqual(self.m_install().rollback_checkpoints.call_count, 1)
|
||||
self.assertEqual(self.m_install().restart.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.client.determine_installer")
|
||||
def test_no_installer(self, mock_det):
|
||||
mock_det.return_value = None
|
||||
|
||||
# Just make sure no exceptions are raised
|
||||
@mock.patch("letsencrypt.client.client.display_ops.pick_installer")
|
||||
def test_no_installer(self, mock_pick_installer):
|
||||
mock_pick_installer.return_value = None
|
||||
self._call(1)
|
||||
# Just make sure no exceptions are raised
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -8,34 +8,35 @@ import mock
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import account
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
from letsencrypt.client.display import util as display_util
|
||||
|
||||
class ChooseAuthenticatorTest(unittest.TestCase):
|
||||
"""Test choose_authenticator function."""
|
||||
|
||||
class ChoosePluginTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.display.ops.choose_plugin."""
|
||||
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
self.mock_apache = mock.Mock()
|
||||
self.mock_stand = mock.Mock()
|
||||
self.mock_apache().more_info.return_value = "Apache Info"
|
||||
self.mock_stand().more_info.return_value = "Standalone Info"
|
||||
self.mock_apache = mock.Mock(
|
||||
description_with_name="a", misconfigured=True)
|
||||
self.mock_stand = mock.Mock(
|
||||
description_with_name="s", misconfigured=False)
|
||||
self.mock_stand.init().more_info.return_value = "standalone"
|
||||
self.plugins = [
|
||||
self.mock_apache,
|
||||
self.mock_stand,
|
||||
]
|
||||
|
||||
self.auths = [self.mock_apache, self.mock_stand]
|
||||
|
||||
self.errs = {self.mock_apache: "This is an error message."}
|
||||
|
||||
@classmethod
|
||||
def _call(cls, auths, errs):
|
||||
from letsencrypt.client.display.ops import choose_authenticator
|
||||
return choose_authenticator(auths, errs)
|
||||
def _call(self):
|
||||
from letsencrypt.client.display.ops import choose_plugin
|
||||
return choose_plugin(self.plugins, "Question?")
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_successful_choice(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 0)
|
||||
|
||||
ret = self._call(self.auths, {})
|
||||
|
||||
self.assertEqual(ret, self.mock_apache)
|
||||
self.assertEqual(self.mock_apache, self._call())
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_more_info(self, mock_util):
|
||||
@@ -45,15 +46,100 @@ class ChooseAuthenticatorTest(unittest.TestCase):
|
||||
(display_util.OK, 1),
|
||||
]
|
||||
|
||||
ret = self._call(self.auths, self.errs)
|
||||
|
||||
self.assertEqual(self.mock_stand, self._call())
|
||||
self.assertEqual(mock_util().notification.call_count, 2)
|
||||
self.assertEqual(ret, self.mock_stand)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_no_choice(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.CANCEL, 0)
|
||||
self.assertTrue(self._call(self.auths, {}) is None)
|
||||
self.assertTrue(self._call() is None)
|
||||
|
||||
|
||||
class PickPluginTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.display.ops.pick_plugin."""
|
||||
|
||||
def setUp(self):
|
||||
self.config = mock.Mock()
|
||||
self.default = None
|
||||
self.reg = mock.MagicMock()
|
||||
self.question = "Question?"
|
||||
self.ifaces = []
|
||||
|
||||
def _call(self):
|
||||
from letsencrypt.client.display.ops import pick_plugin
|
||||
return pick_plugin(self.config, self.default, self.reg,
|
||||
self.question, self.ifaces)
|
||||
|
||||
def test_default_provided(self):
|
||||
self.default = "foo"
|
||||
self._call()
|
||||
self.reg.filter.assert_called_once()
|
||||
|
||||
def test_no_default(self):
|
||||
self._call()
|
||||
self.reg.filter.assert_called_once()
|
||||
|
||||
def test_no_candidate(self):
|
||||
self.assertTrue(self._call() is None)
|
||||
|
||||
def test_single(self):
|
||||
plugin_ep = mock.MagicMock()
|
||||
plugin_ep.init.return_value = "foo"
|
||||
self.reg.ifaces().verify().available.return_value = {"bar": plugin_ep}
|
||||
self.assertEqual("foo", self._call())
|
||||
|
||||
def test_multiple(self):
|
||||
plugin_ep = mock.MagicMock()
|
||||
plugin_ep.init.return_value = "foo"
|
||||
self.reg.ifaces().verify().available.return_value = {
|
||||
"bar": plugin_ep,
|
||||
"baz": plugin_ep,
|
||||
}
|
||||
with mock.patch("letsencrypt.client.display"
|
||||
".ops.choose_plugin") as mock_choose:
|
||||
mock_choose.return_value = plugin_ep
|
||||
self.assertEqual("foo", self._call())
|
||||
mock_choose.assert_called_once_with(
|
||||
[plugin_ep, plugin_ep], self.question)
|
||||
|
||||
def test_choose_plugin_none(self):
|
||||
self.reg.ifaces().verify().available.return_value = {
|
||||
"bar": None,
|
||||
"baz": None,
|
||||
}
|
||||
|
||||
with mock.patch("letsencrypt.client.display"
|
||||
".ops.choose_plugin") as mock_choose:
|
||||
mock_choose.return_value = None
|
||||
self.assertTrue(self._call() is None)
|
||||
|
||||
|
||||
class ConveniencePickPluginTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.display.ops.pick_*."""
|
||||
|
||||
def _test(self, fun, ifaces):
|
||||
config = mock.Mock()
|
||||
default = mock.Mock()
|
||||
plugins = mock.Mock()
|
||||
|
||||
with mock.patch("letsencrypt.client.display.ops.pick_plugin") as mock_p:
|
||||
mock_p.return_value = "foo"
|
||||
self.assertEqual("foo", fun(config, default, plugins, "Question?"))
|
||||
mock_p.assert_called_once_with(
|
||||
config, default, plugins, "Question?", ifaces)
|
||||
|
||||
def test_authenticator(self):
|
||||
from letsencrypt.client.display.ops import pick_authenticator
|
||||
self._test(pick_authenticator, (interfaces.IAuthenticator,))
|
||||
|
||||
def test_installer(self):
|
||||
from letsencrypt.client.display.ops import pick_installer
|
||||
self._test(pick_installer, (interfaces.IInstaller,))
|
||||
|
||||
def test_configurator(self):
|
||||
from letsencrypt.client.display.ops import pick_configurator
|
||||
self._test(pick_configurator, (
|
||||
interfaces.IAuthenticator, interfaces.IInstaller))
|
||||
|
||||
|
||||
class ChooseAccountTest(unittest.TestCase):
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Let's Encrypt scripts."""
|
||||
@@ -1,272 +0,0 @@
|
||||
"""Parse command line and call the appropriate functions.
|
||||
|
||||
.. todo:: Sanity check all input. Be sure to avoid shell code etc...
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import pkg_resources
|
||||
import sys
|
||||
|
||||
import confargparse
|
||||
import zope.component
|
||||
import zope.interface.exceptions
|
||||
import zope.interface.verify
|
||||
|
||||
import letsencrypt
|
||||
|
||||
from letsencrypt.client import account
|
||||
from letsencrypt.client import configuration
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client import client
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import log
|
||||
|
||||
from letsencrypt.client.display import util as display_util
|
||||
from letsencrypt.client.display import ops as display_ops
|
||||
|
||||
|
||||
SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT = "letsencrypt.authenticators"
|
||||
"""Setuptools entry point group name for Authenticator plugins."""
|
||||
|
||||
|
||||
def init_auths(config):
|
||||
"""Find (setuptools entry points) and initialize Authenticators."""
|
||||
# TODO: handle collisions in authenticator names. Or is this
|
||||
# already handled for us by pkg_resources?
|
||||
auths = {}
|
||||
for entrypoint in pkg_resources.iter_entry_points(
|
||||
SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT):
|
||||
auth_cls = entrypoint.load()
|
||||
auth = auth_cls(config)
|
||||
try:
|
||||
zope.interface.verify.verifyObject(interfaces.IAuthenticator, auth)
|
||||
except zope.interface.exceptions.BrokenImplementation:
|
||||
logging.debug(
|
||||
"%r object does not provide IAuthenticator, skipping",
|
||||
entrypoint.name)
|
||||
else:
|
||||
auths[entrypoint.name] = auth
|
||||
return auths
|
||||
|
||||
|
||||
def create_parser():
|
||||
"""Create parser."""
|
||||
parser = confargparse.ConfArgParser(
|
||||
description="letsencrypt client %s" % letsencrypt.__version__)
|
||||
|
||||
add = parser.add_argument
|
||||
config_help = lambda name: interfaces.IConfig[name].__doc__
|
||||
|
||||
add("-d", "--domains", metavar="DOMAIN", nargs="+")
|
||||
add("-s", "--server",
|
||||
default="www.letsencrypt-demo.org/acme/new-reg",
|
||||
help=config_help("server"))
|
||||
|
||||
# TODO: we should generate the list of choices from the set of
|
||||
# available authenticators, but that is tricky due to the
|
||||
# dependency between init_auths and config. Hardcoding it for now.
|
||||
add("-a", "--authenticator", dest="authenticator",
|
||||
help=config_help("authenticator"))
|
||||
|
||||
add("-k", "--authkey", type=read_file,
|
||||
help="Path to the authorized key file")
|
||||
add("-m", "--email", type=str,
|
||||
help="Email address used for account registration.")
|
||||
add("-B", "--rsa-key-size", type=int, default=2048, metavar="N",
|
||||
help=config_help("rsa_key_size"))
|
||||
|
||||
add("-R", "--revoke", action="store_true",
|
||||
help="Revoke a certificate from a menu.")
|
||||
add("--revoke-certificate", dest="rev_cert", type=read_file,
|
||||
help="Revoke a specific certificate.")
|
||||
add("--revoke-key", dest="rev_key", type=read_file,
|
||||
help="Revoke all certs generated by the provided authorized key.")
|
||||
|
||||
add("-b", "--rollback", type=int, default=0, metavar="N",
|
||||
help="Revert configuration N number of checkpoints.")
|
||||
add("-v", "--view-config-changes", action="store_true",
|
||||
help="View checkpoints and associated configuration changes.")
|
||||
|
||||
# TODO: resolve - assumes binary logic while client.py assumes ternary.
|
||||
add("-r", "--redirect", action="store_true",
|
||||
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
|
||||
"authenticated vhost.")
|
||||
|
||||
add("--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="use_curses", action="store_false",
|
||||
help="Use the text output instead of the curses UI.")
|
||||
|
||||
add("--config-dir", default="/etc/letsencrypt",
|
||||
help=config_help("config_dir"))
|
||||
add("--work-dir", default="/var/lib/letsencrypt",
|
||||
help=config_help("work_dir"))
|
||||
add("--backup-dir", default="/var/lib/letsencrypt/backups",
|
||||
help=config_help("backup_dir"))
|
||||
add("--key-dir", default="/etc/letsencrypt/keys",
|
||||
help=config_help("key_dir"))
|
||||
add("--cert-dir", default="/etc/letsencrypt/certs",
|
||||
help=config_help("cert_dir"))
|
||||
|
||||
add("--le-vhost-ext", default="-le-ssl.conf",
|
||||
help=config_help("le_vhost_ext"))
|
||||
add("--cert-path", default="/etc/letsencrypt/certs/cert-letsencrypt.pem",
|
||||
help=config_help("cert_path"))
|
||||
add("--chain-path", default="/etc/letsencrypt/certs/chain-letsencrypt.pem",
|
||||
help=config_help("chain_path"))
|
||||
|
||||
add("--apache-server-root", default="/etc/apache2",
|
||||
help=config_help("apache_server_root"))
|
||||
add("--apache-mod-ssl-conf", default="/etc/letsencrypt/options-ssl.conf",
|
||||
help=config_help("apache_mod_ssl_conf"))
|
||||
add("--apache-ctl", default="apache2ctl", help=config_help("apache_ctl"))
|
||||
add("--apache-enmod", default="a2enmod", help=config_help("apache_enmod"))
|
||||
add("--apache-init-script", default="/etc/init.d/apache2",
|
||||
help=config_help("apache_init_script"))
|
||||
|
||||
add("--nginx-server-root", default="/etc/nginx",
|
||||
help=config_help("nginx_server_root"))
|
||||
add("--nginx-mod-ssl-conf",
|
||||
default="/etc/letsencrypt/options-ssl-nginx.conf",
|
||||
help=config_help("nginx_mod_ssl_conf"))
|
||||
add("--nginx-ctl", default="nginx", help=config_help("nginx_ctl"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(): # pylint: disable=too-many-branches, too-many-statements
|
||||
"""Command line argument parsing and main script execution."""
|
||||
# note: arg parser internally handles --help (and exits afterwards)
|
||||
args = create_parser().parse_args()
|
||||
config = configuration.NamespaceConfig(args)
|
||||
|
||||
# note: check is done after arg parsing as --help should work w/o root also.
|
||||
if not os.geteuid() == 0:
|
||||
sys.exit(
|
||||
"{0}Root is required to run letsencrypt. Please use sudo.{0}"
|
||||
.format(os.linesep))
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
if args.use_curses:
|
||||
logger.addHandler(log.DialogHandler())
|
||||
displayer = display_util.NcursesDisplay()
|
||||
else:
|
||||
displayer = display_util.FileDisplay(sys.stdout)
|
||||
|
||||
zope.component.provideUtility(displayer)
|
||||
|
||||
if args.view_config_changes:
|
||||
client.view_config_changes(config)
|
||||
sys.exit()
|
||||
|
||||
if args.revoke or args.rev_cert is not None or args.rev_key is not None:
|
||||
# This depends on the renewal config and cannot be completed yet.
|
||||
zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
"Revocation is not available with the new Boulder server yet.")
|
||||
|
||||
# client.revoke(config, args.no_confirm, args.rev_cert, args.rev_key)
|
||||
sys.exit()
|
||||
|
||||
if args.rollback > 0:
|
||||
client.rollback(args.rollback, config)
|
||||
sys.exit()
|
||||
|
||||
le_util.make_or_verify_dir(
|
||||
config.config_dir, constants.CONFIG_DIRS_MODE, os.geteuid())
|
||||
|
||||
# Prepare for init of Client
|
||||
if args.email is None:
|
||||
acc = client.determine_account(config)
|
||||
else:
|
||||
try:
|
||||
# The way to get the default would be args.email = ""
|
||||
# First try existing account
|
||||
acc = account.Account.from_existing_account(config, args.email)
|
||||
except errors.LetsEncryptClientError:
|
||||
try:
|
||||
# Try to make an account based on the email address
|
||||
acc = account.Account.from_email(config, args.email)
|
||||
except errors.LetsEncryptClientError:
|
||||
sys.exit(1)
|
||||
|
||||
if acc is None:
|
||||
sys.exit(0)
|
||||
|
||||
all_auths = init_auths(config)
|
||||
logging.debug('Initialized authenticators: %s', all_auths.keys())
|
||||
try:
|
||||
auth = client.determine_authenticator(all_auths, config)
|
||||
logging.debug("Selected authenticator: %s", auth)
|
||||
except errors.LetsEncryptClientError as err:
|
||||
logging.critical(str(err))
|
||||
sys.exit(1)
|
||||
|
||||
if auth is None:
|
||||
sys.exit(0)
|
||||
|
||||
# Use the same object if possible
|
||||
if interfaces.IInstaller.providedBy(auth): # pylint: disable=no-member
|
||||
installer = auth
|
||||
else:
|
||||
# This is simple and avoids confusion right now.
|
||||
installer = None
|
||||
|
||||
if args.domains is None:
|
||||
doms = display_ops.choose_names(installer)
|
||||
else:
|
||||
doms = args.domains
|
||||
|
||||
if not doms:
|
||||
sys.exit(0)
|
||||
|
||||
acme = client.Client(config, acc, auth, installer)
|
||||
|
||||
# Validate the key and csr
|
||||
client.validate_key_csr(acc.key)
|
||||
|
||||
# This more closely mimics the capabilities of the CLI
|
||||
# It should be possible for reconfig only, install-only, no-install
|
||||
# I am not sure the best way to handle all of the unimplemented abilities,
|
||||
# but this code should be safe on all environments.
|
||||
cert_file = None
|
||||
if auth is not None:
|
||||
if acc.regr is None:
|
||||
try:
|
||||
acme.register()
|
||||
except errors.LetsEncryptClientError:
|
||||
sys.exit(0)
|
||||
cert_key, cert_file, chain_file = acme.obtain_certificate(doms)
|
||||
if installer is not None and cert_file is not None:
|
||||
acme.deploy_certificate(doms, cert_key, cert_file, chain_file)
|
||||
if installer is not None:
|
||||
acme.enhance_config(doms, args.redirect)
|
||||
|
||||
|
||||
def read_file(filename):
|
||||
"""Returns the given file's contents with universal new line support.
|
||||
|
||||
:param str filename: Filename
|
||||
|
||||
:returns: A tuple of filename and its contents
|
||||
:rtype: tuple
|
||||
|
||||
:raises argparse.ArgumentTypeError: File does not exist or is not readable.
|
||||
|
||||
"""
|
||||
try:
|
||||
return filename, open(filename, "rU").read()
|
||||
except IOError as exc:
|
||||
raise argparse.ArgumentTypeError(exc.strerror)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://github.com/bw2/ConfigArgParse/issues/17
|
||||
-e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse
|
||||
-e .
|
||||
7
setup.py
7
setup.py
@@ -29,7 +29,7 @@ changes = read_file(os.path.join(here, 'CHANGES.rst'))
|
||||
|
||||
install_requires = [
|
||||
'argparse',
|
||||
'ConfArgParse',
|
||||
'ConfigArgParse',
|
||||
'configobj',
|
||||
'jsonschema',
|
||||
'mock',
|
||||
@@ -112,7 +112,6 @@ setup(
|
||||
'letsencrypt.client.plugins.standalone.tests',
|
||||
'letsencrypt.client.tests',
|
||||
'letsencrypt.client.tests.display',
|
||||
'letsencrypt.scripts',
|
||||
],
|
||||
|
||||
install_requires=install_requires,
|
||||
@@ -127,10 +126,10 @@ setup(
|
||||
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'letsencrypt = letsencrypt.scripts.main:main',
|
||||
'letsencrypt = letsencrypt.client.cli:main',
|
||||
'jws = letsencrypt.acme.jose.jws:CLI.run',
|
||||
],
|
||||
'letsencrypt.authenticators': [
|
||||
'letsencrypt.plugins': [
|
||||
'apache = letsencrypt.client.plugins.apache.configurator'
|
||||
':ApacheConfigurator',
|
||||
'nginx = letsencrypt.client.plugins.nginx.configurator'
|
||||
|
||||
Reference in New Issue
Block a user