diff --git a/docs/index.rst b/docs/index.rst index 34615168c..72be096f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,7 @@ Welcome to the Let's Encrypt client documentation! intro using contributing + plugins .. toctree:: :maxdepth: 1 diff --git a/docs/plugins.rst b/docs/plugins.rst new file mode 100644 index 000000000..0451bfe3f --- /dev/null +++ b/docs/plugins.rst @@ -0,0 +1,19 @@ +======= +Plugins +======= + +Let's Encrypt client supports dynamic discovery of plugins through the +`setuptools entry points`_. This way you can, for example, create a +custom implementation of +`~letsencrypt.client.interfaces.IAuthenticator` or the +'~letsencrypt.client.interfaces.IInstaller' without having to +merge it with the core upstream source code. An example is provided in +``examples/plugins/`` directory. + +Please be aware though that as this client is still in a developer-preview +stage, the API may undergo a few changes. If you believe the plugin will be +beneficial to the community, please consider submitting a pull request to the +repo and we will update it with any necessary API changes. + +.. _`setuptools entry points`: + https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins diff --git a/examples/plugins/letsencrypt_example_plugins.py b/examples/plugins/letsencrypt_example_plugins.py new file mode 100644 index 000000000..6817c7f1d --- /dev/null +++ b/examples/plugins/letsencrypt_example_plugins.py @@ -0,0 +1,16 @@ +"""Example Let's Encrypt plugins.""" +import zope.interface + +from letsencrypt.client import interfaces + + +class Authenticator(object): + zope.interface.implements(interfaces.IAuthenticator) + + description = 'Example Authenticator plugin' + + def __init__(self, config): + self.config = config + + # Implement all methods from IAuthenticator, remembering to add + # "self" as first argument, e.g. def prepare(self)... diff --git a/examples/plugins/setup.py b/examples/plugins/setup.py new file mode 100644 index 000000000..845d6eb66 --- /dev/null +++ b/examples/plugins/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + + +setup( + name='letsencrypt-example-plugins', + package='letsencrypt_example_plugins.py', + install_requires=[ + 'letsencrypt', + 'zope.interface', + ], + entry_points={ + 'letsencrypt.authenticators': [ + 'example = letsencrypt_example_plugins:Authenticator', + ], + }, +) diff --git a/letsencrypt/client/standalone_authenticator.py b/letsencrypt/client/standalone_authenticator.py index bf08a39ec..22597eba7 100644 --- a/letsencrypt/client/standalone_authenticator.py +++ b/letsencrypt/client/standalone_authenticator.py @@ -33,7 +33,7 @@ class StandaloneAuthenticator(object): description = "Standalone Authenticator" - def __init__(self): + def __init__(self, unused_config): self.child_pid = None self.parent_pid = os.getpid() self.subproc_state = None diff --git a/letsencrypt/client/tests/standalone_authenticator_test.py b/letsencrypt/client/tests/standalone_authenticator_test.py index 9adf6a167..62b955e7e 100644 --- a/letsencrypt/client/tests/standalone_authenticator_test.py +++ b/letsencrypt/client/tests/standalone_authenticator_test.py @@ -51,7 +51,7 @@ class ChallPrefTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) def test_chall_pref(self): self.assertEqual(self.authenticator.get_chall_pref("example.com"), @@ -63,7 +63,7 @@ class SNICallbackTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( __name__, "testdata/rsa256_key.pem") key = le_util.Key("foo", test_key) @@ -106,7 +106,7 @@ class ClientSignalHandlerTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) self.authenticator.tasks = {"foononce.acme.invalid": "stuff"} self.authenticator.child_pid = 12345 @@ -135,7 +135,7 @@ class SubprocSignalHandlerTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) self.authenticator.tasks = {"foononce.acme.invalid": "stuff"} self.authenticator.child_pid = 12345 self.authenticator.parent_pid = 23456 @@ -187,7 +187,7 @@ class AlreadyListeningTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) @mock.patch("letsencrypt.client.standalone_authenticator.psutil." "net_connections") @@ -290,7 +290,7 @@ class PerformTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( __name__, "testdata/rsa256_key.pem") @@ -367,7 +367,7 @@ class StartListenerTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) @mock.patch("letsencrypt.client.standalone_authenticator." "Crypto.Random.atfork") @@ -402,7 +402,7 @@ class DoParentProcessTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) @mock.patch("letsencrypt.client.standalone_authenticator.signal.signal") @mock.patch("letsencrypt.client.standalone_authenticator." @@ -452,7 +452,7 @@ class DoChildProcessTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( __name__, "testdata/rsa256_key.pem") key = le_util.Key("foo", test_key) @@ -545,7 +545,7 @@ class CleanupTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) self.achall = achallenges.DVSNI( chall=challenges.DVSNI(r="whee", nonce="foononce"), domain="foo.example.com", key="key") @@ -575,7 +575,7 @@ class MoreInfoTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import ( StandaloneAuthenticator) - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) def test_more_info(self): """Make sure exceptions aren't raised.""" @@ -587,7 +587,7 @@ class InitTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import ( StandaloneAuthenticator) - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) def test_prepare(self): """Make sure exceptions aren't raised. diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 11caf944a..3b4b7c10d 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -11,6 +11,8 @@ import sys import confargparse import zope.component +import zope.interface.exceptions +import zope.interface.verify import letsencrypt @@ -20,12 +22,32 @@ from letsencrypt.client import errors from letsencrypt.client import interfaces from letsencrypt.client import le_util from letsencrypt.client import log -from letsencrypt.client import standalone_authenticator as standalone -from letsencrypt.client.apache import configurator 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.""" + 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[auth] = entrypoint.name + return auths + + def create_parser(): """Create parser.""" parser = confargparse.ConfArgParser( @@ -136,12 +158,10 @@ def main(): # pylint: disable=too-many-branches, too-many-statements if not args.eula: display_eula() - all_auths = [ - configurator.ApacheConfigurator(config), - standalone.StandaloneAuthenticator(), - ] + all_auths = init_auths(config) + logging.debug('Initialized authenticators: %s', all_auths.values()) try: - auth = client.determine_authenticator(all_auths) + auth = client.determine_authenticator(all_auths.keys()) except errors.LetsEncryptClientError: logging.critical("No authentication mechanisms were found on your " "system.") diff --git a/setup.py b/setup.py index fac3eef90..c07c1f2ce 100644 --- a/setup.py +++ b/setup.py @@ -119,6 +119,12 @@ setup( 'letsencrypt = letsencrypt.scripts.main:main', 'jws = letsencrypt.acme.jose.jws:CLI.run', ], + 'letsencrypt.authenticators': [ + 'apache = letsencrypt.client.apache.configurator' + ':ApacheConfigurator', + 'standalone = letsencrypt.client.standalone_authenticator' + ':StandaloneAuthenticator', + ], }, zip_safe=False,