diff --git a/certbot-example-plugins/certbot_example_plugins.py b/certbot-example-plugins/certbot_example_plugins.py deleted file mode 100644 index 9dec2e1..0000000 --- a/certbot-example-plugins/certbot_example_plugins.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Example Certbot plugins. - -For full examples, see `certbot.plugins`. - -""" -import zope.interface - -from certbot import interfaces -from certbot.plugins import common - - -@zope.interface.implementer(interfaces.IAuthenticator) -@zope.interface.provider(interfaces.IPluginFactory) -class Authenticator(common.Plugin): - """Example Authenticator.""" - - description = "Example Authenticator plugin" - - # Implement all methods from IAuthenticator, remembering to add - # "self" as first argument, e.g. def prepare(self)... - - -@zope.interface.implementer(interfaces.IInstaller) -@zope.interface.provider(interfaces.IPluginFactory) -class Installer(common.Plugin): - """Example Installer.""" - - description = "Example Installer plugin" - - # Implement all methods from IInstaller, remembering to add - # "self" as first argument, e.g. def get_all_names(self)... diff --git a/certbot-example-plugins/setup.py b/certbot-example-plugins/setup.py deleted file mode 100644 index 4538e83..0000000 --- a/certbot-example-plugins/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -from setuptools import setup - - -setup( - name='certbot-example-plugins', - package='certbot_example_plugins.py', - install_requires=[ - 'certbot', - 'zope.interface', - ], - entry_points={ - 'certbot.plugins': [ - 'example_authenticator = certbot_example_plugins:Authenticator', - 'example_installer = certbot_example_plugins:Installer', - ], - }, -) diff --git a/notes.txt b/notes.txt index 2dc389a..9ae16b0 100644 --- a/notes.txt +++ b/notes.txt @@ -1,3 +1,4 @@ -docker run -it --rm --entrypoint /bin/sh -v "$(pwd)/certbot-example-plugins:/tmp/certbot-example-plugins" certbot/certbot:v0.22.0 +docker run -it --rm --entrypoint /bin/sh -v "$(pwd)/plugin:/tmp/plugin" certbot/certbot:v0.22.0 certbot plugins -pip install -e /tmp/certbot-example-plugins +pip install -e /tmp/plugin +certbot certonly --dry-run -a certbot-plugin-gandi:dns --certbot-plugin-gandi:dns-credentials /tmp/creds.ini -d domain.com diff --git a/plugin/certbot_plugin_gandi/__init__.py b/plugin/certbot_plugin_gandi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin/certbot_plugin_gandi/gandi_api.py b/plugin/certbot_plugin_gandi/gandi_api.py new file mode 100644 index 0000000..e76c6ba --- /dev/null +++ b/plugin/certbot_plugin_gandi/gandi_api.py @@ -0,0 +1,99 @@ +import requests +import urllib +from collections import namedtuple +from certbot.plugins import dns_common + + +_GandiConfig = namedtuple('_GandiConfig', ('api_key',)) +_BaseDomain = namedtuple('_BaseDomain', ('zone_uuid', 'fqdn')) + + +def get_config(api_key): + return _GandiConfig(api_key=api_key) + + +def _get_json(response): + try: + data = response.json() + except ValueError: + return dict() + return data + + +def _get_response_message(response, default=''): + return _get_json(response).get('message', default) + + +def _headers(cfg): + return { + 'Content-Type': 'application/json', + 'X-Api-Key': cfg.api_key + } + + +def _get_url(*segs): + return 'https://dns.api.gandi.net/api/v5/{}'.format( + '/'.join(urllib.quote(seg, safe='') for seg in segs) + ) + + +def _request(cfg, method, segs, **kw): + headers = _headers(cfg) + url = _get_url(*segs) + return requests.request(method, url, headers=headers, **kw) + + +def _get_base_domain(cfg, domain): + for candidate_base_domain in dns_common.base_domain_name_guesses(domain): + response = _request(cfg, 'GET', ('domains', candidate_base_domain)) + if response.ok: + data = _get_json(response) + zone_uuid = data.get('zone_uuid') + fqdn = data.get('fqdn') + if zone_uuid and fqdn: + return _BaseDomain(zone_uuid=zone_uuid, fqdn=fqdn) + return None + + +def _get_relative_name(base_domain, name): + suffix = '.' + base_domain.fqdn + return name[:-len(suffix)] if name.endswith(suffix) else None + + +def _del_txt_record(cfg, base_domain, relative_name): + return _request(cfg, 'DELETE', ('zones', base_domain.zone_uuid, 'records', relative_name, 'TXT')) + + +def _update_record(cfg, domain, name, request_runner): + + base_domain = _get_base_domain(cfg, domain) + if base_domain is None: + return 'Unable to get base domain for "{}"'.format(domain) + relative_name = _get_relative_name(base_domain, name) + if relative_name is None: + return 'Unable to derive relative name for "{}"'.format(name) + + response = request_runner(base_domain, relative_name) + + return None if response.ok else _get_response_message(response) + + +def add_txt_record(cfg, domain, name, value): + + def requester(base_domain, relative_name): + _del_txt_record(cfg, base_domain, relative_name) + return _request(cfg, 'POST', + ('zones', base_domain.zone_uuid, 'records', relative_name, 'TXT'), + json={'rrset_values': [value]}) + + return _update_record(cfg, domain, name, requester) + + +def del_txt_record(cfg, domain, name): + + def requester(base_domain, relative_name): + return _del_txt_record(cfg, base_domain, relative_name) + + return _update_record(cfg, domain, name, requester) + + diff --git a/plugin/certbot_plugin_gandi/main.py b/plugin/certbot_plugin_gandi/main.py new file mode 100644 index 0000000..d0ab250 --- /dev/null +++ b/plugin/certbot_plugin_gandi/main.py @@ -0,0 +1,60 @@ +import zope.interface +import logging + +from certbot import interfaces, errors +from certbot.plugins import dns_common + +from . import gandi_api + + +logger = logging.getLogger(__name__) + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for Gandi (using LiveDNS).""" + + description = 'Obtain certificates using a DNS TXT record (if you are using Gandi for DNS).' + + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.credentials = None + + + @classmethod + def add_parser_arguments(cls, add): # pylint: disable=arguments-differ + super(Authenticator, cls).add_parser_arguments(add) + add('credentials', help='Gandi credentials INI file.') + + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ + 'the Gandi LiveDNS API.' + + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'Gandi credentials INI file', + { + 'api-key': 'API key for Gandi account' + } + ) + + + def _perform(self, domain, validation_name, validation): + error = gandi_api.add_txt_record(self._get_gandi_config(), domain, validation_name, validation) + if error is not None: + raise errors.PluginError('An error occurred adding the DNS TXT record: {0}'.format(error)) + + + def _cleanup(self, domain, validation_name, validation): + error = gandi_api.del_txt_record(self._get_gandi_config(), domain, validation_name) + if error is not None: + logger.warn('Unable to find or delete the DNS TXT record: %s', error) + + + def _get_gandi_config(self): + return gandi_api.get_config(api_key = self.credentials.conf('api-key')) diff --git a/plugin/setup.py b/plugin/setup.py new file mode 100644 index 0000000..665052d --- /dev/null +++ b/plugin/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup, find_packages + + +setup( + name='certbot-plugin-gandi', + packages=find_packages(), + install_requires=[ + 'certbot', + 'zope.interface', + 'requests>=2.4.2', + ], + entry_points={ + 'certbot.plugins': [ + 'dns = certbot_plugin_gandi.main:Authenticator', + ], + }, +)