Use Gandi LiveDNS to add and remove the challenge records

This commit is contained in:
Michael Porter 2018-03-13 18:08:03 +00:00
parent 0e2e4c9be7
commit 56b693eab4
7 changed files with 179 additions and 50 deletions

View File

@ -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)...

View File

@ -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',
],
},
)

View File

@ -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

View File

View File

@ -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='<No reason given>'):
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)

View File

@ -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'))

17
plugin/setup.py Normal file
View File

@ -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',
],
},
)