- compatible with python3.6

- adapted to allow for wildcard-certs
- adapted to allow for more than one Order per domain
This commit is contained in:
Markus Pawlata 2019-02-24 01:38:31 +01:00
parent 6d423a99cb
commit 476530bd4f
3 changed files with 57 additions and 24 deletions

View File

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -2,6 +2,10 @@ import requests
import urllib import urllib
from collections import namedtuple from collections import namedtuple
from certbot.plugins import dns_common 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',)) _GandiConfig = namedtuple('_GandiConfig', ('api_key',))
@ -33,7 +37,7 @@ def _headers(cfg):
def _get_url(*segs): def _get_url(*segs):
return 'https://dns.api.gandi.net/api/v5/{}'.format( 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): 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): 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) 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 add_txt_record(cfg, domain, name, value):
def requester(base_domain, relative_name): def requester(base_domain, relative_name):
_del_txt_record(cfg, base_domain, relative_name) _del_txt_record(cfg, base_domain, relative_name)
return _request(cfg, 'POST', return _request(
('zones', base_domain.zone_uuid, 'records', relative_name, 'TXT'), cfg,
json={'rrset_values': [value]}) '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) 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 _del_txt_record(cfg, base_domain, relative_name)
return _update_record(cfg, domain, name, requester) return _update_record(cfg, domain, name, requester)

View File

@ -1,7 +1,8 @@
import zope.interface import zope.interface
import logging import logging
from certbot import interfaces, errors from certbot import interfaces
from certbot.errors import PluginError
from certbot.plugins import dns_common from certbot.plugins import dns_common
from . import gandi_api from . import gandi_api
@ -15,24 +16,21 @@ logger = logging.getLogger(__name__)
class Authenticator(dns_common.DNSAuthenticator): class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for Gandi (using LiveDNS).""" """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): def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs) super(Authenticator, self).__init__(*args, **kwargs)
self.credentials = None self.credentials = None
@classmethod @classmethod
def add_parser_arguments(cls, add): # pylint: disable=arguments-differ def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
super(Authenticator, cls).add_parser_arguments(add) super(Authenticator, cls).add_parser_arguments(add)
add('credentials', help='Gandi credentials INI file.') add('credentials', help='Gandi credentials INI file.')
def more_info(self): # pylint: disable=missing-docstring,no-self-use 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 ' + \ return ('This plugin configures a DNS TXT record to respond to a '
'the Gandi LiveDNS API.' 'dns-01 challenge using the Gandi LiveDNS API.')
def _setup_credentials(self): def _setup_credentials(self):
self.credentials = self._configure_credentials( self.credentials = self._configure_credentials(
@ -43,18 +41,25 @@ class Authenticator(dns_common.DNSAuthenticator):
} }
) )
def _perform(self, domain, validation_name, validation): 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: 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): 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: 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): 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'))