From 0b76e29b65b2bf011f6f5017670b88faa2751525 Mon Sep 17 00:00:00 2001 From: Matthias Bilger Date: Thu, 15 Aug 2019 18:46:50 +0200 Subject: [PATCH] Initial Version of plugin --- LICENSE.txt | 190 +++++++++++++++++ README.rst | 7 + certbot_dns_ispconfig/__init__.py | 86 ++++++++ certbot_dns_ispconfig/dns_ispconfig.py | 220 ++++++++++++++++++++ certbot_dns_ispconfig/dns_ispconfig_test.py | 137 ++++++++++++ setup.cfg | 2 + setup.py | 58 ++++++ 7 files changed, 700 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.rst create mode 100644 certbot_dns_ispconfig/__init__.py create mode 100644 certbot_dns_ispconfig/dns_ispconfig.py create mode 100644 certbot_dns_ispconfig/dns_ispconfig_test.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..981c46c --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2015 Electronic Frontier Foundation and others + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d36ee48 --- /dev/null +++ b/README.rst @@ -0,0 +1,7 @@ +ISPConfig DNS Authenticator plugin for Certbot + +In the `System -> Remote Users` you have to have a user, with the following rights + +- Client Functions +- DNS zone functions +- DNS txt functions diff --git a/certbot_dns_ispconfig/__init__.py b/certbot_dns_ispconfig/__init__.py new file mode 100644 index 0000000..242e914 --- /dev/null +++ b/certbot_dns_ispconfig/__init__.py @@ -0,0 +1,86 @@ +""" +The `~certbot_dns_ispconfig.dns_ispconfig` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the ISPConfig REST API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-ispconfig-credentials`` ISPConfig Remote API credentials_ + INI file. (Required) +``--dns-ispconfig-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 120) +======================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing ISPConfig Remote API +credentials, obtained from your DNSimple +`System > Remote Users`. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # ISPCONFIG API credentials used by Certbot + dns_ispconfig_username = myispremoteuser + dns_ispconfig_password = mysecretpassword + dns_ispconfig_endpoint = https://localhost:8080 + +The path to this file can be provided interactively or using the +``--dns-ispconfig-credentials`` command-line argument. Certbot records the path +to this file for use during renewal, but does not store the file's contents. + +.. caution:: + You should protect these API credentials as you would a password. Users who + can read this file can use these credentials to issue arbitrary API calls on + your behalf. Users who can cause Certbot to run using these credentials can + complete a ``dns-01`` challenge to acquire new certificates or revoke + existing certificates for associated domains, even if those domains aren't + being managed by this server. + +Certbot will emit a warning if it detects that the credentials file can be +accessed by other users on your system. The warning reads "Unsafe permissions +on credentials configuration file", followed by the path to the credentials +file. This warning will be emitted each time Certbot uses the credentials file, +including for renewal, and cannot be silenced except by addressing the issue +(e.g., by using a command like ``chmod 600`` to restrict access to the file). + +Examples +-------- + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com`` + + certbot certonly \\ + --dns-ispconfig \\ + --dns-ispconfig-credentials ~/.secrets/certbot/ispconfig.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-ispconfig \\ + --dns-ispconfig-credentials ~/.secrets/certbot/ispconfig.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 240 seconds + for DNS propagation + + certbot certonly \\ + --dns-ispconfig \\ + --dns-ispconfig-credentials ~/.secrets/certbot/ispconfig.ini \\ + --dns-ispconfig-propagation-seconds 240 \\ + -d example.com + +""" diff --git a/certbot_dns_ispconfig/dns_ispconfig.py b/certbot_dns_ispconfig/dns_ispconfig.py new file mode 100644 index 0000000..49ea47e --- /dev/null +++ b/certbot_dns_ispconfig/dns_ispconfig.py @@ -0,0 +1,220 @@ +"""DNS Authenticator for ISPConfig.""" +import json +import logging + +import requests +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 ISPConfig + + This Authenticator uses the ISPConfig Remote REST API to fulfill a dns-01 challenge. + """ + + description = ('Obtain certificates using a DNS TXT record (if you are using ISPConfig for DNS).') + ttl = 60 + + 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, default_propagation_seconds=120) + add('credentials', help='ISPConfig 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 ISPConfig Remote REST API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'ISPConfig credentials INI file', + { + 'endpoint': 'URL of the ISPConfig Remote API.', + 'username': 'Username for ISPConfig Remote API.', + 'password': 'Password for ISPConfig Remote API.' + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_ispconfig_client().add_txt_record(domain, validation_name, validation, self.ttl) + + def _cleanup(self, domain, validation_name, validation): + self._get_ispconfig_client().del_txt_record(domain, validation_name, validation, self.ttl) + + def _get_ispconfig_client(self): + return _ISPConfigClient(self.credentials.conf('endpoint'), self.credentials.conf('username'), self.credentials.conf('password')) + + +class _ISPConfigClient(object): + """ + Encapsulates all communication with the ISPConfig Remote REST API. + """ + + def __init__(self, endpoint, username, password): + self.endpoint = endpoint + self.username = username + self.password = password + self.session = requests.Session() + self.session_id = None + + def _login(self): + if self.session_id is not None: + return + logindata = {'username': self.username, 'password':self.password} + self.session_id = self._api_request('login', logindata) + + def _api_request(self, action, data): + if self.session_id is not None: + data['session_id'] = self.session_id + resp = self.session.get( + self._get_url(action), + json=data + ) + if resp.status_code != 200: + raise errors.PluginError('HTTP Error during login {0}'.format(resp.status_code)) + try: + result = resp.json() + except: + raise errors.PluginError('API response with non JSON: {0}'.format(resp.text)) + if (result['code'] == 'ok'): + return result['response'] + elif (result['code'] == 'remote_fault'): + raise errors.PluginError('API response with an error: {0}'.format(result['message'])) + else: + raise errors.PluginError('API response unknown {0}'.format(resp.text)) + + def _get_url(self, action): + return '{0}?{1}'.format(self.endpoint, action) + + def _get_server_id(self, zone_id): + zone = self._api_request('dns_zone_get', {'primary_id': zone_id}) + return zone['server_id'] + + def add_txt_record(self, domain, record_name, record_content, record_ttl): + """ + Add a TXT record using the supplied information. + + :param str domain: The domain to use to look up the managed zone. + :param str record_name: The record name (typically beginning with '_acme-challenge.'). + :param str record_content: The record content (typically the challenge validation). + :param int record_ttl: The record TTL (number of seconds that the record may be cached). + :raises certbot.errors.PluginError: if an error occurs communicating with the ISPConfig API + """ + self._login() + zone_id = self._find_managed_zone_id(domain) + if zone_id is None: + raise errors.PluginError("Domain not known") + record = self.get_existing_txt(zone_id, record_name) + if record is not None: + if record['data'] == record_content: + print('already there, id {0}'.format(record['id'])) + return + else: + self._update_txt_record(zone_id, record['id'], record_name, record_content, record_ttl) + else: + self._insert_txt_record(zone_id, record_name, record_content, record_ttl) + + def del_txt_record(self, domain, record_name, record_content, record_ttl): + """ + Delete a TXT record using the supplied information. + + :param str domain: The domain to use to look up the managed zone. + :param str record_name: The record name (typically beginning with '_acme-challenge.'). + :param str record_content: The record content (typically the challenge validation). + :param int record_ttl: The record TTL (number of seconds that the record may be cached). + :raises certbot.errors.PluginError: if an error occurs communicating with the ISPConfig API + """ + self._login() + zone_id = self._find_managed_zone_id(domain) + if zone_id is None: + raise errors.PluginError("Domain not known") + record = self.get_existing_txt(zone_id, record_name) + if record is not None: + if record['data'] == record_content: + self._delete_txt_record(record['id']) + + def _prepare_rr_data(self, zone_id, record_name, record_content, record_ttl): + server_id = self._get_server_id(zone_id) + data = { + 'client_id': None, + 'rr_type': 'TXT', + 'params':{ + 'server_id': server_id, + 'name': record_name, + 'active': 'Y', + 'type': 'TXT', + 'data': record_content, + 'zone': zone_id, + 'ttl': record_ttl, + 'update_serial':False, + }, + } + return data + + def _insert_txt_record(self, zone_id, record_name, record_content, record_ttl): + data = self._prepare_rr_data(zone_id, record_name, record_content, record_ttl) + result = self._api_request('dns_txt_add', data) + + def _update_txt_record(self, zone_id, primary_id, record_name, record_content, record_ttl): + data = self._prepare_rr_data(zone_id, record_name, record_content, record_ttl) + data['primary_id'] = primary_id + result = self._api_request('dns_txt_update', data) + + def _delete_txt_record(self, primary_id): + data = { 'primary_id': primary_id } + result = self._api_request('dns_txt_delete', data) + + def _find_managed_zone_id(self, domain): + """ + Find the managed zone for a given domain. + + :param str domain: The domain for which to find the managed zone. + :returns: The ID of the managed zone, if found. + :rtype: str + :raises certbot.errors.PluginError: if the managed zone cannot be found. + """ + + zone_dns_name_guesses = dns_common.base_domain_name_guesses(domain) + + for zone_name in zone_dns_name_guesses: + #get the zone id + try: + zone_id = self._api_request('dns_zone_get_id', {'origin': zone_name}) + return zone_id + except errors.PluginError as e: + pass + return None + + def get_existing_txt(self, zone_id, record_name): + """ + Get existing TXT records from the RRset for the record name. + + If an error occurs while requesting the record set, it is suppressed + and None is returned. + + :param str zone_id: The ID of the managed zone. + :param str record_name: The record name (typically beginning with '_acme-challenge.'). + + :returns: TXT record value or None + :rtype: `string` or `None` + + """ + self._login() + read_zone_data = {'zone_id': zone_id} + zone_data = self._api_request('dns_rr_get_all_by_zone', read_zone_data) + for entry in zone_data: + if entry['name'] == record_name and entry['type'] == 'TXT': + return entry + return None diff --git a/certbot_dns_ispconfig/dns_ispconfig_test.py b/certbot_dns_ispconfig/dns_ispconfig_test.py new file mode 100644 index 0000000..6f27767 --- /dev/null +++ b/certbot_dns_ispconfig/dns_ispconfig_test.py @@ -0,0 +1,137 @@ +"""Tests for certbot_dns_ispconfig.dns_ispconfig.""" + +import unittest + +import mock +import json +import requests_mock + +from certbot import errors +from certbot.compat import os +from certbot.errors import PluginError +from certbot.plugins import dns_test_common +from certbot.plugins.dns_test_common import DOMAIN +from certbot.tests import util as test_util + +FAKE_USER = "remoteuser" +FAKE_PW = "password" +FAKE_ENDPOINT = 'mock://endpoint' + + +class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_ispconfig.dns_ispconfig import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write({ + "ispconfig_username": FAKE_USER, + "ispconfig_password": FAKE_PW, + "ispconfig_endpoint": FAKE_ENDPOINT, + }, path) + + super(AuthenticatorTest, self).setUp() + self.config = mock.MagicMock(ispconfig_credentials=path, + ispconfig_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "ispconfig") + + self.mock_client = mock.MagicMock() + # _get_ispconfig_client | pylint: disable=protected-access + self.auth._get_ispconfig_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, 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, mock.ANY)] + self.assertEqual(expected, self.mock_client.mock_calls) + + +class ISPConfigClientTest(unittest.TestCase): + record_name = "foo" + record_content = "bar" + record_ttl = 42 + + def setUp(self): + from certbot_dns_ispconfig.dns_ispconfig import _ISPConfigClient + + self.adapter = requests_mock.Adapter() + + self.client = _ISPConfigClient(FAKE_ENDPOINT, FAKE_USER, FAKE_PW) + self.client.session.mount('mock', self.adapter) + + def _register_response(self, ep_id, response=None, message=None, additional_matcher=None, **kwargs): + resp = {"code":"ok", + "message":message, + "response":response} + if message is not None: + resp['code'] = "remote_failure" + + def add_matcher(request): + data = json.loads(request.text) + add_result = True + if additional_matcher is not None: + add_result = additionsal_matcher(request) + + return ((('username' in data and data['username'] == FAKE_USER) and + ('username' in data and data['password'] == FAKE_PW)) or + data['session_id'] == 'FAKE_SESSION') and add_result + + self.adapter.register_uri( + requests_mock.ANY, + '{0}?{1}'.format(FAKE_ENDPOINT, ep_id), + text=json.dumps(resp), + additional_matcher=add_matcher, + **kwargs + ) + + def test_add_txt_record(self): + if request_mock.__version__ < '1.6.0' + self._register_response('login', response='FAKE_SESSION') + self._register_response('dns_zone_get_id', response=23) + self._register_response('dns_txt_add', response=99) + self._register_response('dns_zone_get', response={'zone_id': 102, 'server_id': 1}) + self._register_response('dns_rr_get_all_by_zone', response=[]) + self.client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) + + def test_add_txt_record_fail_to_find_domain(self): + self._register_response('login', response='FAKE_SESSION') + self._register_response('dns_zone_get_id', message='Not Found') + with self.assertRaises(errors.PluginError) as context: + self.client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) + + def test_add_txt_record_fail_to_authenticate(self): + self._register_response('login', message='FAILED') + with self.assertRaises(errors.PluginError) as context: + self.client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) + + def test_del_txt_record(self): + self._register_response('login', response='FAKE_SESSION') + self._register_response('dns_zone_get_id', response=23) + self._register_response('dns_rr_get_all_by_zone', response=[]) + self._register_response('dns_txt_delete', response='') + self.client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) + + def test_del_txt_record_fail_to_find_domain(self): + self._register_response('login', response='FAKE_SESSION') + self._register_response('dns_zone_get_id', message='Not Found') + with self.assertRaises(errors.PluginError) as context: + self.client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) + + def test_del_txt_record_fail_to_authenticate(self): + self._register_response('login', message='FAILED') + with self.assertRaises(errors.PluginError) as context: + self.client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0567798 --- /dev/null +++ b/setup.py @@ -0,0 +1,58 @@ +from setuptools import setup +from setuptools import find_packages + + +version = '0.1.0' + +# Remember to update local-oldest-requirements.txt when changing the minimum +# acme/certbot version. +install_requires = [ + 'acme>=0.29.0', + 'certbot>=0.34.0', + 'setuptools', + 'requests', + 'mock', + 'requests-mock', +] + +setup( + name='certbot-dns-ispconfig', + version=version, + description="ispconfig DNS Authenticator plugin for Certbot", + url='https://github.com/certbot/certbot', + author="Certbot Project", + author_email='client-dev@letsencrypt.org', + license='Apache License 2.0', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Plugins', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Security', + 'Topic :: System :: Installation/Setup', + 'Topic :: System :: Networking', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities', + ], + + packages=find_packages(), + include_package_data=True, + install_requires=install_requires, + entry_points={ + 'certbot.plugins': [ + 'dns-ispconfig = certbot_dns_ispconfig.dns_ispconfig:Authenticator', + ], + }, + test_suite='certbot_dns_ispconfig', +)