diff --git a/LICENSE b/LICENSE index f4a7df0..8a746d0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Michael Porter +Copyright (c) 2018 Michael Porter, with modifications from Markus Pawlata Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/certbot_plugin_gandi/gandi_api.py b/certbot_plugin_gandi/gandi_api.py index e76c6ba..f733887 100644 --- a/certbot_plugin_gandi/gandi_api.py +++ b/certbot_plugin_gandi/gandi_api.py @@ -2,6 +2,10 @@ import requests import urllib from collections import namedtuple from certbot.plugins import dns_common +try: + from urllib import quote # Python 2.X +except ImportError: + from urllib.parse import quote # Python 3+ _GandiConfig = namedtuple('_GandiConfig', ('api_key',)) @@ -33,7 +37,7 @@ def _headers(cfg): def _get_url(*segs): return 'https://dns.api.gandi.net/api/v5/{}'.format( - '/'.join(urllib.quote(seg, safe='') for seg in segs) + '/'.join(quote(seg, safe='') for seg in segs) ) @@ -61,7 +65,10 @@ def _get_relative_name(base_domain, name): def _del_txt_record(cfg, base_domain, relative_name): - return _request(cfg, 'DELETE', ('zones', base_domain.zone_uuid, 'records', relative_name, 'TXT')) + return _request( + cfg, + 'DELETE', + ('zones', base_domain.zone_uuid, 'records', relative_name, 'TXT')) def _update_record(cfg, domain, name, request_runner): @@ -78,13 +85,36 @@ def _update_record(cfg, domain, name, request_runner): return None if response.ok else _get_response_message(response) +def get_txt_records(cfg, domain, name): + + 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( + cfg, + 'GET', + ('zones', base_domain.zone_uuid, 'records', relative_name, 'TXT')) + if response.ok: + return response.json().get('rrset_values') + else: + return [] + + 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 _request( + cfg, + 'POST', + ('zones', base_domain.zone_uuid, 'records', relative_name, 'TXT'), + json={ + 'rrset_values': value if isinstance(value, list) else [value] + }) return _update_record(cfg, domain, name, requester) @@ -95,5 +125,3 @@ def del_txt_record(cfg, domain, name): return _del_txt_record(cfg, base_domain, relative_name) return _update_record(cfg, domain, name, requester) - - diff --git a/certbot_plugin_gandi/main.py b/certbot_plugin_gandi/main.py index d0ab250..4f90b70 100644 --- a/certbot_plugin_gandi/main.py +++ b/certbot_plugin_gandi/main.py @@ -1,7 +1,8 @@ import zope.interface import logging -from certbot import interfaces, errors +from certbot import interfaces +from certbot.errors import PluginError from certbot.plugins import dns_common from . import gandi_api @@ -15,24 +16,21 @@ logger = logging.getLogger(__name__) 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).' - + 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.' - + 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( @@ -43,18 +41,25 @@ class Authenticator(dns_common.DNSAuthenticator): } ) - def _perform(self, domain, validation_name, validation): - error = gandi_api.add_txt_record(self._get_gandi_config(), domain, validation_name, validation) + previous_values = gandi_api.get_txt_records( + self._get_gandi_config(), domain, validation_name) + previous_values.append(validation) + error = gandi_api.add_txt_record( + self._get_gandi_config(), domain, validation_name, previous_values) if error is not None: - raise errors.PluginError('An error occurred adding the DNS TXT record: {0}'.format(error)) - + raise PluginError( + 'An error occurred adding the DNS TXT record: {}'.format( + error)) def _cleanup(self, domain, validation_name, validation): - error = gandi_api.del_txt_record(self._get_gandi_config(), domain, validation_name) + 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) - + logger.warn( + ('Unable to find or delete the DNS TXT record: {}, this ' + 'is normal if you had multiple challenges in the same ' + 'zone.').format(error)) def _get_gandi_config(self): - return gandi_api.get_config(api_key = self.credentials.conf('api-key')) + return gandi_api.get_config(api_key=self.credentials.conf('api-key'))