Use Gandi LiveDNS to add and remove the challenge records
This commit is contained in:
		
							
								
								
									
										0
									
								
								plugin/certbot_plugin_gandi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								plugin/certbot_plugin_gandi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										99
									
								
								plugin/certbot_plugin_gandi/gandi_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								plugin/certbot_plugin_gandi/gandi_api.py
									
									
									
									
									
										Normal 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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										60
									
								
								plugin/certbot_plugin_gandi/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								plugin/certbot_plugin_gandi/main.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										17
									
								
								plugin/setup.py
									
									
									
									
									
										Normal 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',
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
)
 | 
			
		||||
		Reference in New Issue
	
	Block a user