From ff532469a56875ff24cc8aada7215ee2700111ab Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 13:55:23 +0000 Subject: [PATCH 1/6] Setuptools entry_points plugins --- docs/index.rst | 1 + docs/plugins.rst | 5 ++++ .../plugins/letsencrypt_example_plugins.py | 16 +++++++++++ examples/plugins/setup.py | 16 +++++++++++ letsencrypt/client/client.py | 27 +++++++++++++++++++ .../client/standalone_authenticator.py | 2 +- .../tests/standalone_authenticator_test.py | 24 ++++++++--------- letsencrypt/scripts/main.py | 8 +++--- setup.py | 6 +++++ 9 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 docs/plugins.rst create mode 100644 examples/plugins/letsencrypt_example_plugins.py create mode 100644 examples/plugins/setup.py 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..552985aab --- /dev/null +++ b/docs/plugins.rst @@ -0,0 +1,5 @@ +======= +Plugins +======= + +You can find an example in ``examples/plugins/`` directory. 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/client.py b/letsencrypt/client/client.py index 2f3f9a769..a448c10ce 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -1,11 +1,15 @@ """ACME protocol client class and helper functions.""" import logging import os +import pkg_resources import sys import Crypto.PublicKey.RSA import M2Crypto +import zope.interface.exceptions +import zope.interface.verify + from letsencrypt.acme import messages from letsencrypt.acme.jose import util as jose_util @@ -13,6 +17,7 @@ from letsencrypt.client import auth_handler from letsencrypt.client import client_authenticator from letsencrypt.client import crypto_util from letsencrypt.client import errors +from letsencrypt.client import interfaces from letsencrypt.client import le_util from letsencrypt.client import network from letsencrypt.client import reverter @@ -23,6 +28,28 @@ from letsencrypt.client.display import ops as display_ops from letsencrypt.client.display import enhancements +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( + '"%s" object does not provide IAuthenticator, skipping', + entrypoint.name) + else: + auths[auth] = entrypoint.name + return auths + + class Client(object): """ACME protocol client. 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..d3c2318d6 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -136,12 +136,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 = client.init_auths(config) + logging.debug('Initialized authenticators: %s', all_auths) 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, From 03383c38241bbb4fb0b7b1c438c5652937c8140d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 13:59:33 +0000 Subject: [PATCH 2/6] Fix quotes --- letsencrypt/client/client.py | 4 ++-- letsencrypt/scripts/main.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index a448c10ce..01f5e1c80 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -28,7 +28,7 @@ from letsencrypt.client.display import ops as display_ops from letsencrypt.client.display import enhancements -SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT = 'letsencrypt.authenticators' +SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT = "letsencrypt.authenticators" """Setuptools entry point group name for Authenticator plugins.""" @@ -43,7 +43,7 @@ def init_auths(config): zope.interface.verify.verifyObject(interfaces.IAuthenticator, auth) except zope.interface.exceptions.BrokenImplementation: logging.debug( - '"%s" object does not provide IAuthenticator, skipping', + "%r object does not provide IAuthenticator, skipping", entrypoint.name) else: auths[auth] = entrypoint.name diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index d3c2318d6..d51288c3a 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -137,7 +137,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements display_eula() all_auths = client.init_auths(config) - logging.debug('Initialized authenticators: %s', all_auths) + logging.debug('Initialized authenticators: %s', all_auths.values()) try: auth = client.determine_authenticator(all_auths.keys()) except errors.LetsEncryptClientError: From d9871b59f032bb64ddd5d65b6dfbb7619d3c7cc5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 14:42:07 +0000 Subject: [PATCH 3/6] pylint: unused imports --- letsencrypt/scripts/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index d51288c3a..8b2c62935 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -20,8 +20,6 @@ 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 From 8e32ae82467a0d2597eb835a3ed8a1d5546e72ad Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 22:00:00 +0000 Subject: [PATCH 4/6] Move init_auths to scripts/main.py. --- letsencrypt/client/client.py | 27 --------------------------- letsencrypt/scripts/main.py | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 01f5e1c80..2f3f9a769 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -1,15 +1,11 @@ """ACME protocol client class and helper functions.""" import logging import os -import pkg_resources import sys import Crypto.PublicKey.RSA import M2Crypto -import zope.interface.exceptions -import zope.interface.verify - from letsencrypt.acme import messages from letsencrypt.acme.jose import util as jose_util @@ -17,7 +13,6 @@ from letsencrypt.client import auth_handler from letsencrypt.client import client_authenticator from letsencrypt.client import crypto_util from letsencrypt.client import errors -from letsencrypt.client import interfaces from letsencrypt.client import le_util from letsencrypt.client import network from letsencrypt.client import reverter @@ -28,28 +23,6 @@ from letsencrypt.client.display import ops as display_ops from letsencrypt.client.display import enhancements -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 - - class Client(object): """ACME protocol client. diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 8b2c62935..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 @@ -24,6 +26,28 @@ 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( @@ -134,7 +158,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements if not args.eula: display_eula() - all_auths = client.init_auths(config) + all_auths = init_auths(config) logging.debug('Initialized authenticators: %s', all_auths.values()) try: auth = client.determine_authenticator(all_auths.keys()) From 6b78789ea3963ba406538d733f9ff65db8337fa7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 22:12:40 +0000 Subject: [PATCH 5/6] Improve plugins.rst --- docs/plugins.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index 552985aab..fafb8d5d3 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -2,4 +2,12 @@ Plugins ======= -You can find an example in ``examples/plugins/`` directory. +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` without having to +merge it with the core upstream source code. Example is provided in +``examples/plugins/`` directory. + +.. _`setuptools entry points`: + https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins From 8fa2204afe8b6ee6fe758f51f438495ca17e0659 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 27 Mar 2015 12:29:27 -0700 Subject: [PATCH 6/6] Add disclaimer in plugins doc --- docs/plugins.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index fafb8d5d3..0451bfe3f 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -5,9 +5,15 @@ 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` without having to -merge it with the core upstream source code. Example is provided in +`~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