Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
21 changed files
with
687 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Godaddy DNS Authenticator""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
"""DNS Authenticator for Godaddy.""" | ||
import logging | ||
|
||
import godaddypy | ||
import zope.interface | ||
|
||
from certbot import errors | ||
from certbot import interfaces | ||
from certbot.plugins import dns_common | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@zope.interface.implementer(interfaces.IAuthenticator) | ||
@zope.interface.provider(interfaces.IPluginFactory) | ||
class Authenticator(dns_common.DNSAuthenticator): | ||
"""DNS Authenticator for Godaddy | ||
This Authenticator uses the Godaddy API to fulfill a dns-01 challenge. | ||
""" | ||
|
||
description = 'Obtain certs using a DNS TXT record (if you are using Godaddy 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='Godaddy 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 Godaddy API.' | ||
|
||
def _setup_credentials(self): | ||
self.credentials = self._configure_credentials( | ||
'credentials', | ||
'Godaddy credentials INI file', | ||
{ | ||
'key': 'API key for Godaddy account', | ||
'secret': 'API secret for Godaddy account' | ||
} | ||
) | ||
|
||
def _perform(self, domain, validation_name, validation): | ||
self._get_godaddy_client().add_txt_record(domain, validation_name, validation) | ||
|
||
def _cleanup(self, domain, validation_name, validation): | ||
self._get_godaddy_client().del_txt_record(domain, validation_name, validation) | ||
|
||
def _get_godaddy_client(self): | ||
return _GodaddyClient(self.credentials.conf('key'), self.credentials.conf('secret')) | ||
|
||
|
||
class _GodaddyClient(object): | ||
""" | ||
Encapsulates all communication with the Godaddy API. | ||
""" | ||
|
||
def __init__(self, key, secret): | ||
account = godaddypy.Account(api_key=key, api_secret=secret) | ||
self.client = godaddypy.Client(account) | ||
|
||
def add_txt_record(self, domain_name, record_name, record_content): | ||
""" | ||
Add a TXT record using the supplied information. | ||
:param str domain_name: The domain to use to associate the record with. | ||
:param str record_name: The record name (typically beginning with '_acme-challenge.'). | ||
:param str record_content: The record content (typically the challenge validation). | ||
:raises certbot.errors.PluginError: if an error occurs communicating with the Godaddy | ||
API | ||
""" | ||
|
||
try: | ||
domain = self._find_domain(domain_name) | ||
except godaddypy.client.BadResponse as e: | ||
hint = None | ||
|
||
if "UNABLE_TO_AUTHENTICATE" in str(e): | ||
hint = 'Did you provide a valid API token?' | ||
|
||
logger.debug('Error finding domain using the Godaddy API: %s', e) | ||
raise errors.PluginError('Error finding domain using the Godaddy API: {0}{1}' | ||
.format(e, ' ({0})'.format(hint) if hint else '')) | ||
|
||
try: | ||
self.client.add_record(domain, { | ||
'data': record_content, | ||
'name': dns_common.compute_record_name(domain, record_name), | ||
'type': 'TXT' | ||
}) | ||
logger.debug('Successfully added TXT record') | ||
except godaddypy.client.BadResponse as e: | ||
logger.debug('Error adding TXT record using the Godaddy API: %s', e) | ||
raise errors.PluginError('Error adding TXT record using the Godaddy API: {0}' | ||
.format(e)) | ||
|
||
def del_txt_record(self, domain_name, record_name, record_content): | ||
""" | ||
Delete a TXT record using the supplied information. | ||
Note that both the record's name and content are used to ensure that similar records | ||
created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted. | ||
Failures are logged, but not raised. | ||
:param str domain_name: The domain to use to associate the record with. | ||
:param str record_name: The record name (typically beginning with '_acme-challenge.'). | ||
:param str record_content: The record content (typically the challenge validation). | ||
""" | ||
|
||
try: | ||
domain = self._find_domain(domain_name) | ||
except godaddypy.client.BadResponse as e: | ||
logger.debug('Error finding domain using the Godaddy API: %s', e) | ||
return | ||
|
||
try: | ||
domain_records = self.client.get_records(domain, record_type='TXT') | ||
|
||
matching_records = [record for record in domain_records | ||
if record['type'] == 'TXT' | ||
and record['name'] == dns_common.compute_record_name(domain, | ||
record_name) | ||
and record['data'] == record_content] | ||
except godaddypy.client.BadResponse as e: | ||
logger.debug('Error getting DNS records using the Godaddy API: %s', e) | ||
return | ||
|
||
for record in matching_records: | ||
try: | ||
logger.debug('Removing TXT record with name: %s', record['name']) | ||
self.client.delete_records(domain, name=record['name'], record_type='TXT') | ||
except godaddypy.client.BadResponse as e: | ||
logger.warn('Error deleting TXT record %s using the Godaddy API: %s', | ||
record['name'], e) | ||
|
||
def _find_domain(self, domain_name): | ||
""" | ||
Find the domain object for a given domain name. | ||
:param str domain_name: The domain name for which to find the corresponding Domain. | ||
:returns: The Domain, if found. | ||
:rtype: str | ||
:raises certbot.errors.PluginError: if no matching Domain is found. | ||
""" | ||
|
||
domain_name_guesses = dns_common.base_domain_name_guesses(domain_name) | ||
|
||
domains = self.client.get_domains() | ||
|
||
for guess in domain_name_guesses: | ||
matches = [domain for domain in domains if domain == guess] | ||
|
||
if len(matches) > 0: | ||
domain = matches[0] | ||
logger.debug('Found base domain for %s using name %s', domain_name, guess) | ||
return domain | ||
|
||
raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.' | ||
.format(domain_name, domain_name_guesses)) |
138 changes: 138 additions & 0 deletions
138
certbot-dns-godaddy/certbot_dns_godaddy/dns_godaddy_test.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
"""Tests for certbot_dns_godaddy.dns_godaddy.""" | ||
|
||
import os | ||
import unittest | ||
|
||
import godaddypy | ||
import mock | ||
|
||
from certbot import errors | ||
from certbot.plugins import dns_test_common | ||
from certbot.plugins.dns_test_common import DOMAIN | ||
from certbot.tests import util as test_util | ||
|
||
API_ERROR = godaddypy.client.BadResponse('UNABLE_TO_AUTHENTICATE') | ||
KEY = 'a-key' | ||
SECRET = 'a-secret' | ||
|
||
|
||
class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): | ||
|
||
def setUp(self): | ||
from certbot_dns_godaddy.dns_godaddy import Authenticator | ||
|
||
super(AuthenticatorTest, self).setUp() | ||
|
||
path = os.path.join(self.tempdir, 'file.ini') | ||
dns_test_common.write({"godaddy_key": KEY, "godaddy_secret": SECRET}, path) | ||
|
||
self.config = mock.MagicMock(godaddy_credentials=path, | ||
godaddy_propagation_seconds=0) # don't wait during tests | ||
|
||
self.auth = Authenticator(self.config, "godaddy") | ||
|
||
self.mock_client = mock.MagicMock() | ||
# _get_godaddy_client | pylint: disable=protected-access | ||
self.auth._get_godaddy_client = mock.MagicMock(return_value=self.mock_client) | ||
|
||
def test_perform(self): | ||
self.auth.perform([self.achall]) | ||
|
||
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] | ||
self.assertEqual(expected, self.mock_client.mock_calls) | ||
|
||
def test_cleanup(self): | ||
# _attempt_cleanup | pylint: disable=protected-access | ||
self.auth._attempt_cleanup = True | ||
self.auth.cleanup([self.achall]) | ||
|
||
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] | ||
self.assertEqual(expected, self.mock_client.mock_calls) | ||
|
||
|
||
class GodaddyClientTest(unittest.TestCase): | ||
id = 1 | ||
record_prefix = "_acme-challenge" | ||
record_name = record_prefix + "." + DOMAIN | ||
record_content = "bar" | ||
|
||
def setUp(self): | ||
from certbot_dns_godaddy.dns_godaddy import _GodaddyClient | ||
|
||
self.godaddy_client = _GodaddyClient(KEY, SECRET) | ||
|
||
self.client = mock.MagicMock() | ||
self.godaddy_client.client = self.client | ||
|
||
def test_add_txt_record(self): | ||
self.client.get_domains.return_value = [DOMAIN] | ||
|
||
self.godaddy_client.add_txt_record(DOMAIN, self.record_name, self.record_content) | ||
|
||
self.client.add_record.assert_called_with(DOMAIN, {'type': 'TXT', | ||
'name': self.record_prefix, | ||
'data': self.record_content}) | ||
|
||
def test_add_txt_record_fail_to_find_domain(self): | ||
self.client.get_domains.return_value = [] | ||
|
||
self.assertRaises(errors.PluginError, | ||
self.godaddy_client.add_txt_record, | ||
DOMAIN, self.record_name, self.record_content) | ||
|
||
def test_add_txt_record_error_finding_domain(self): | ||
self.client.get_domains.side_effect = API_ERROR | ||
|
||
self.assertRaises(errors.PluginError, | ||
self.godaddy_client.add_txt_record, | ||
DOMAIN, self.record_name, self.record_content) | ||
|
||
def test_add_txt_record_error_creating_record(self): | ||
self.client.get_domains.return_value = [DOMAIN] | ||
self.client.add_record.side_effect = API_ERROR | ||
|
||
self.assertRaises(errors.PluginError, | ||
self.godaddy_client.add_txt_record, | ||
DOMAIN, self.record_name, self.record_content) | ||
|
||
def test_del_txt_record(self): | ||
records = [ | ||
{'type': 'TXT', 'name': 'DIFFERENT', 'data': self.record_content}, | ||
{'type': 'TXT', 'name': self.record_prefix, 'data': self.record_content}, | ||
{'type': 'TXT', 'name': self.record_prefix, 'data': 'DIFFERENT'}, | ||
] | ||
|
||
self.client.get_domains.return_value = [DOMAIN] | ||
self.client.get_records.return_value = records | ||
|
||
self.godaddy_client.del_txt_record(DOMAIN, self.record_name, self.record_content) | ||
|
||
self.client.delete_records.assert_called_with(DOMAIN, name=self.record_prefix, | ||
record_type='TXT') | ||
|
||
def test_del_txt_record_error_finding_domain(self): | ||
self.client.get_domains.side_effect = API_ERROR | ||
|
||
self.godaddy_client.del_txt_record(DOMAIN, self.record_name, self.record_content) | ||
|
||
def test_del_txt_record_error_getting_records(self): | ||
self.client.get_domains.return_value = [DOMAIN] | ||
self.client.get_records.side_effect = API_ERROR | ||
|
||
self.godaddy_client.del_txt_record(DOMAIN, self.record_name, self.record_content) | ||
|
||
def test_del_txt_record_error_deleting_records(self): | ||
records = [ | ||
{'type': 'TXT', 'name': 'DIFFERENT', 'data': self.record_content}, | ||
{'type': 'TXT', 'name': self.record_prefix, 'data': self.record_content}, | ||
{'type': 'TXT', 'name': self.record_prefix, 'data': 'DIFFERENT'}, | ||
] | ||
|
||
self.client.get_domains.return_value = [DOMAIN] | ||
self.client.get_records.return_value = records | ||
self.client.delete_records.side_effect = API_ERROR | ||
|
||
self.godaddy_client.del_txt_record(DOMAIN, self.record_name, self.record_content) | ||
|
||
if __name__ == "__main__": | ||
unittest.main() # pragma: no cover |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/_build/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Minimal makefile for Sphinx documentation | ||
# | ||
|
||
# You can set these variables from the command line. | ||
SPHINXOPTS = | ||
SPHINXBUILD = sphinx-build | ||
SPHINXPROJ = certbot-dns-godaddy | ||
SOURCEDIR = . | ||
BUILDDIR = _build | ||
|
||
# Put it first so that "make" without argument is like "make help". | ||
help: | ||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) | ||
|
||
.PHONY: help Makefile | ||
|
||
# Catch-all target: route all unknown targets to Sphinx using the new | ||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). | ||
%: Makefile | ||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
================= | ||
API Documentation | ||
================= | ||
|
||
.. toctree:: | ||
:glob: | ||
|
||
api/** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
:mod:`certbot_dns_godaddy.dns_godaddy` | ||
------------------------------------------------ | ||
|
||
.. automodule:: certbot_dns_godaddy.dns_godaddy | ||
:members: |
Oops, something went wrong.