From 2564566e1c512619f71bb8bbdc77821907a46ebe Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Jul 2018 23:19:29 +0300 Subject: [PATCH 01/19] Do not call IPlugin.prepare() for updaters when running renew (#6167) interfaces.GenericUpdater and new enhancement interface updater functions get run on every invocation of Certbot with "renew" verb for every lineage. This causes performance problems for users with large configurations, because of plugin plumbing and preparsing happening in prepare() method of installer plugins. This PR moves the responsibility to call prepare() to the plugin (possibly) implementing a new style enhancement interface. Fixes: #6153 * Do not call IPlugin.prepare() for updaters when running renew * Check prepare called in tests * Refine pydoc and make the function name more informative * Verify the plugin type --- certbot-apache/certbot_apache/configurator.py | 5 ++ .../certbot_apache/tests/autohsts_test.py | 5 +- certbot/interfaces.py | 3 ++ certbot/main.py | 2 +- certbot/plugins/enhancements.py | 7 ++- certbot/plugins/selection.py | 29 +++++++++++ certbot/plugins/selection_test.py | 48 ++++++++++++++++++- certbot/tests/main_test.py | 14 +++--- certbot/tests/renewupdater_test.py | 22 +++++---- certbot/updater.py | 10 ++-- 10 files changed, 119 insertions(+), 26 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index ab83a5332..bb77e2e41 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -165,6 +165,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]] # These will be set in the prepare function + self._prepared = False self.parser = None self.version = version self.vhosts = None @@ -249,6 +250,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): logger.debug("Encountered error:", exc_info=True) raise errors.PluginError( "Unable to lock %s", self.conf("server-root")) + self._prepared = True def _check_aug_version(self): """ Checks that we have recent enough version of libaugeas. @@ -2394,6 +2396,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): continue nextstep = config["laststep"] + 1 if nextstep < len(constants.AUTOHSTS_STEPS): + # If installer hasn't been prepared yet, do it now + if not self._prepared: + self.prepare() # Have not reached the max value yet try: vhost = self.find_vhost_by_id(id_str) diff --git a/certbot-apache/certbot_apache/tests/autohsts_test.py b/certbot-apache/certbot_apache/tests/autohsts_test.py index 86d985079..73da33f15 100644 --- a/certbot-apache/certbot_apache/tests/autohsts_test.py +++ b/certbot-apache/certbot_apache/tests/autohsts_test.py @@ -55,7 +55,9 @@ class AutoHSTSTest(util.ApacheTest): @mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0) @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") - def test_autohsts_increase(self, _mock_restart): + @mock.patch("certbot_apache.configurator.ApacheConfigurator.prepare") + def test_autohsts_increase(self, mock_prepare, _mock_restart): + self.config._prepared = False maxage = "\"max-age={0}\"" initial_val = maxage.format(constants.AUTOHSTS_STEPS[0]) inc_val = maxage.format(constants.AUTOHSTS_STEPS[1]) @@ -69,6 +71,7 @@ class AutoHSTSTest(util.ApacheTest): # Verify increased value self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), inc_val) + self.assertTrue(mock_prepare.called) @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") @mock.patch("certbot_apache.configurator.ApacheConfigurator._autohsts_increase") diff --git a/certbot/interfaces.py b/certbot/interfaces.py index a5fb426e6..2e837d1d2 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -620,6 +620,9 @@ class GenericUpdater(object): methods, and interfaces.GenericUpdater.register(InstallerClass) should be called from the installer code. + The plugins implementing this enhancement are responsible of handling + the saving of configuration checkpoints as well as other calls to + interface methods of `interfaces.IInstaller` such as prepare() and restart() """ @abc.abstractmethod diff --git a/certbot/main.py b/certbot/main.py index 6078f87a6..556722104 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1199,11 +1199,11 @@ def renew_cert(config, plugins, lineage): # In case of a renewal, reload server to pick up new certificate. # In principle we could have a configuration option to inhibit this # from happening. + # Run deployer updater.run_renewal_deployer(config, renewed_lineage, installer) installer.restart() notify("new certificate deployed with reload of {0} server; fullchain is {1}".format( config.installer, lineage.fullchain), pause=False) - # Run deployer def certonly(config, plugins): """Authenticate & obtain cert, but do not install it. diff --git a/certbot/plugins/enhancements.py b/certbot/plugins/enhancements.py index 506abe433..7ca096610 100644 --- a/certbot/plugins/enhancements.py +++ b/certbot/plugins/enhancements.py @@ -88,7 +88,8 @@ class AutoHSTSEnhancement(object): The plugins implementing new style enhancements are responsible of handling the saving of configuration checkpoints as well as calling possible restarts - of managed software themselves. + of managed software themselves. For update_autohsts method, the installer may + have to call prepare() to finalize the plugin initialization. Methods: enable_autohsts is called when the header is initially installed using a @@ -112,6 +113,10 @@ class AutoHSTSEnhancement(object): :param lineage: Certificate lineage object :type lineage: certbot.storage.RenewableCert + + .. note:: prepare() method inherited from `interfaces.IPlugin` might need + to be called manually within implementation of this interface method + to finalize the plugin initialization. """ @abc.abstractmethod diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 030d5b6db..95f123a46 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -39,6 +39,35 @@ def pick_authenticator( return pick_plugin( config, default, plugins, question, (interfaces.IAuthenticator,)) +def get_unprepared_installer(config, plugins): + """ + Get an unprepared interfaces.IInstaller object. + + :param certbot.interfaces.IConfig config: Configuration + :param certbot.plugins.disco.PluginsRegistry plugins: + All plugins registered as entry points. + + :returns: Unprepared installer plugin or None + :rtype: IPlugin or None + """ + + _, req_inst = cli_plugin_requests(config) + if not req_inst: + return None + installers = plugins.filter(lambda p_ep: p_ep.name == req_inst) + installers.init(config) + installers = installers.verify((interfaces.IInstaller,)) + if len(installers) > 1: + raise errors.PluginSelectionError( + "Found multiple installers with the name %s, Certbot is unable to " + "determine which one to use. Skipping." % req_inst) + if installers: + inst = list(installers.values())[0] + logger.debug("Selecting plugin: %s", inst) + return inst.init(config) + else: + raise errors.PluginSelectionError( + "Could not select or initialize the requested installer %s." % req_inst) def pick_plugin(config, default, plugins, question, ifaces): """Pick plugin. diff --git a/certbot/plugins/selection_test.py b/certbot/plugins/selection_test.py index ab480544a..44d64ab8e 100644 --- a/certbot/plugins/selection_test.py +++ b/certbot/plugins/selection_test.py @@ -6,10 +6,13 @@ import unittest import mock import zope.component +from certbot import errors +from certbot import interfaces + from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module from certbot.display import util as display_util +from certbot.plugins.disco import PluginsRegistry from certbot.tests import util as test_util -from certbot import interfaces class ConveniencePickPluginTest(unittest.TestCase): @@ -170,5 +173,48 @@ class ChoosePluginTest(unittest.TestCase): self.assertTrue("default" in mock_util().menu.call_args[1]) +class GetUnpreparedInstallerTest(test_util.ConfigTestCase): + """Tests for certbot.plugins.selection.get_unprepared_installer.""" + + def setUp(self): + super(GetUnpreparedInstallerTest, self).setUp() + self.mock_apache_fail_ep = mock.Mock( + description_with_name="afail") + self.mock_apache_fail_ep.name = "afail" + self.mock_apache_ep = mock.Mock( + description_with_name="apache") + self.mock_apache_ep.name = "apache" + self.mock_apache_plugin = mock.MagicMock() + self.mock_apache_ep.init.return_value = self.mock_apache_plugin + self.plugins = PluginsRegistry({ + "afail": self.mock_apache_fail_ep, + "apache": self.mock_apache_ep, + }) + + def _call(self): + from certbot.plugins.selection import get_unprepared_installer + return get_unprepared_installer(self.config, self.plugins) + + def test_no_installer_defined(self): + self.config.configurator = None + self.assertEquals(self._call(), None) + + def test_no_available_installers(self): + self.config.configurator = "apache" + self.plugins = PluginsRegistry({}) + self.assertRaises(errors.PluginSelectionError, self._call) + + def test_get_plugin(self): + self.config.configurator = "apache" + installer = self._call() + self.assertTrue(installer is self.mock_apache_plugin) + + def test_multiple_installers_returned(self): + self.config.configurator = "apache" + # Two plugins with the same name + self.mock_apache_fail_ep.name = "apache" + self.assertRaises(errors.PluginSelectionError, self._call) + + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index a2115a486..cc4e6c293 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1493,17 +1493,17 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met self.assertTrue(mock_handle.called) @mock.patch('certbot.plugins.selection.choose_configurator_plugins') - def test_plugin_selection_error(self, mock_choose): + @mock.patch('certbot.updater._run_updaters') + def test_plugin_selection_error(self, mock_run, mock_choose): mock_choose.side_effect = errors.PluginSelectionError self.assertRaises(errors.PluginSelectionError, main.renew_cert, None, None, None) - with mock.patch('certbot.updater.logger.warning') as mock_log: - self.config.dry_run = False - updater.run_generic_updaters(self.config, None, None) - self.assertTrue(mock_log.called) - self.assertTrue("Could not choose appropriate plugin for updaters" - in mock_log.call_args[0][0]) + self.config.dry_run = False + updater.run_generic_updaters(self.config, None, None) + # Make sure we're returning None, and hence not trying to run the + # without installer + self.assertFalse(mock_run.called) class UnregisterTest(unittest.TestCase): diff --git a/certbot/tests/renewupdater_test.py b/certbot/tests/renewupdater_test.py index c1b97843e..5a362072c 100644 --- a/certbot/tests/renewupdater_test.py +++ b/certbot/tests/renewupdater_test.py @@ -23,13 +23,15 @@ class RenewUpdaterTest(test_util.ConfigTestCase): @mock.patch('certbot.main._get_and_save_cert') @mock.patch('certbot.plugins.selection.choose_configurator_plugins') + @mock.patch('certbot.plugins.selection.get_unprepared_installer') @test_util.patch_get_utility() - def test_server_updates(self, _, mock_select, mock_getsave): + def test_server_updates(self, _, mock_geti, mock_select, mock_getsave): mock_getsave.return_value = mock.MagicMock() mock_generic_updater = self.generic_updater # Generic Updater mock_select.return_value = (mock_generic_updater, None) + mock_geti.return_value = mock_generic_updater with mock.patch('certbot.main._init_le_client'): main.renew_cert(self.config, None, mock.MagicMock()) self.assertTrue(mock_generic_updater.restart.called) @@ -62,9 +64,9 @@ class RenewUpdaterTest(test_util.ConfigTestCase): self.assertEquals(mock_log.call_args[0][0], "Skipping renewal deployer in dry-run mode.") - @mock.patch('certbot.plugins.selection.choose_configurator_plugins') - def test_enhancement_updates(self, mock_select): - mock_select.return_value = (self.mockinstaller, None) + @mock.patch('certbot.plugins.selection.get_unprepared_installer') + def test_enhancement_updates(self, mock_geti): + mock_geti.return_value = self.mockinstaller updater.run_generic_updaters(self.config, mock.MagicMock(), None) self.assertTrue(self.mockinstaller.update_autohsts.called) self.assertEqual(self.mockinstaller.update_autohsts.call_count, 1) @@ -74,10 +76,10 @@ class RenewUpdaterTest(test_util.ConfigTestCase): self.mockinstaller) self.assertTrue(self.mockinstaller.deploy_autohsts.called) - @mock.patch('certbot.plugins.selection.choose_configurator_plugins') - def test_enhancement_updates_not_called(self, mock_select): + @mock.patch('certbot.plugins.selection.get_unprepared_installer') + def test_enhancement_updates_not_called(self, mock_geti): self.config.disable_renew_updates = True - mock_select.return_value = (self.mockinstaller, None) + mock_geti.return_value = self.mockinstaller updater.run_generic_updaters(self.config, mock.MagicMock(), None) self.assertFalse(self.mockinstaller.update_autohsts.called) @@ -87,8 +89,8 @@ class RenewUpdaterTest(test_util.ConfigTestCase): self.mockinstaller) self.assertFalse(self.mockinstaller.deploy_autohsts.called) - @mock.patch('certbot.plugins.selection.choose_configurator_plugins') - def test_enhancement_no_updater(self, mock_select): + @mock.patch('certbot.plugins.selection.get_unprepared_installer') + def test_enhancement_no_updater(self, mock_geti): FAKEINDEX = [ { "name": "Test", @@ -98,7 +100,7 @@ class RenewUpdaterTest(test_util.ConfigTestCase): "enable_function": "enable_autohsts" } ] - mock_select.return_value = (self.mockinstaller, None) + mock_geti.return_value = self.mockinstaller with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX): updater.run_generic_updaters(self.config, mock.MagicMock(), None) self.assertFalse(self.mockinstaller.update_autohsts.called) diff --git a/certbot/updater.py b/certbot/updater.py index fb7c52f77..58df6fcb4 100644 --- a/certbot/updater.py +++ b/certbot/updater.py @@ -28,13 +28,13 @@ def run_generic_updaters(config, lineage, plugins): logger.debug("Skipping updaters in dry-run mode.") return try: - # installers are used in auth mode to determine domain names - installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "certonly") - except errors.PluginSelectionError as e: + installer = plug_sel.get_unprepared_installer(config, plugins) + except errors.Error as e: logger.warning("Could not choose appropriate plugin for updaters: %s", e) return - _run_updaters(lineage, installer, config) - _run_enhancement_updaters(lineage, installer, config) + if installer: + _run_updaters(lineage, installer, config) + _run_enhancement_updaters(lineage, installer, config) def run_renewal_deployer(config, lineage, installer): """Helper function to run deployer interface method if supported by the used From dd600db436542f79363f1fbdc50e1a2f6cf0ba42 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 Jul 2018 09:16:08 -0700 Subject: [PATCH 02/19] Upgrade pinned josepy version (#6184) We released josepy 1.1.0 a while ago to work around newer versions of cryptography deprecating some of the functionality we were using. We haven't yet upgraded our pinned josepy version though and since #6169 has landed, we're now seeing these deprecation warnings in our tests. This would be shown to certbot-auto users as well. This PR removes these warnings by upgrading our pinned version of josepy. * update pinned josepy version * build leauto * update pinned dev version of josepy --- letsencrypt-auto-source/letsencrypt-auto | 6 +++--- letsencrypt-auto-source/pieces/dependency-requirements.txt | 6 +++--- tools/dev_constraints.txt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index d571a5a8d..9afa86849 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -1092,9 +1092,9 @@ idna==2.5 \ ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 -josepy==1.0.1 \ - --hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \ - --hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc +josepy==1.1.0 \ + --hash=sha256:1309a25aac3caeff5239729c58ff9b583f7d022ffdb1553406ddfc8e5b52b76e \ + --hash=sha256:fb5c62c77d26e04df29cb5ecd01b9ce69b6fcc9e521eb1ca193b7faa2afa7086 linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt index 54498cb3e..ae6079d96 100644 --- a/letsencrypt-auto-source/pieces/dependency-requirements.txt +++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt @@ -96,9 +96,9 @@ idna==2.5 \ ipaddress==1.0.16 \ --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 -josepy==1.0.1 \ - --hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \ - --hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc +josepy==1.1.0 \ + --hash=sha256:1309a25aac3caeff5239729c58ff9b583f7d022ffdb1553406ddfc8e5b52b76e \ + --hash=sha256:fb5c62c77d26e04df29cb5ecd01b9ce69b6fcc9e521eb1ca193b7faa2afa7086 linecache2==1.0.0 \ --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index 777222ffb..4a392e0b4 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -26,7 +26,7 @@ ipython==5.5.0 ipython-genutils==0.2.0 Jinja2==2.9.6 jmespath==0.9.3 -josepy==1.0.1 +josepy==1.1.0 logger==1.4 logilab-common==1.4.1 MarkupSafe==1.0 From cdf93de33899a95a544bbf7b99ddc1c327cc2728 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 Jul 2018 09:16:44 -0700 Subject: [PATCH 03/19] Full Python 3.7 support (#6182) Now that yaml/pyyaml#126 is resolved, #6170 can be reverted by bumping the pinned version of PyYAML. You can see this code passing with full macOS and integration tests at https://travis-ci.org/certbot/certbot/builds/400957729. * Revert "Allow py37 testing (#6170)" This reverts commit cad95466b05e6be51c1c29eaa91e6e3b7ea3cefd. * Bump pyyaml pinning to work on Python 3.7. --- tools/dev_constraints.txt | 2 +- tox.ini | 22 ++++------------------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index 4a392e0b4..5a16b8cba 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -51,7 +51,7 @@ pytest-forked==0.2 pytest-xdist==1.20.1 python-dateutil==2.6.1 python-digitalocean==1.11 -PyYAML==3.12 +PyYAML==3.13 repoze.sphinx.autointerface==0.8 requests-file==1.4.2 requests-toolbelt==0.8.0 diff --git a/tox.ini b/tox.ini index b44d30449..ef71b52be 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,8 @@ pip_install = {toxinidir}/tools/pip_install_editable.sh # before the script moves on to the next package. All dependencies are pinned # to a specific version for increased stability for developers. install_and_test = {toxinidir}/tools/install_and_test.sh -python37_compatible_dns_packages = +dns_packages = + certbot-dns-cloudflare \ certbot-dns-cloudxns \ certbot-dns-digitalocean \ certbot-dns-dnsimple \ @@ -24,22 +25,14 @@ python37_compatible_dns_packages = certbot-dns-nsone \ certbot-dns-rfc2136 \ certbot-dns-route53 -dns_packages = - certbot-dns-cloudflare \ - {[base]python37_compatible_dns_packages} -nondns_packages = +all_packages = acme[dev] \ .[dev] \ certbot-apache \ + {[base]dns_packages} \ certbot-nginx \ certbot-postfix \ letshelp-certbot -python37_compatible_packages = - {[base]nondns_packages} \ - {[base]python37_compatible_dns_packages} -all_packages = - {[base]nondns_packages} \ - {[base]dns_packages} install_packages = {toxinidir}/tools/pip_install_editable.sh {[base]all_packages} source_paths = @@ -69,13 +62,6 @@ commands = setenv = PYTHONHASHSEED = 0 -[testenv:py37] -commands = - {[base]install_and_test} {[base]python37_compatible_packages} - python tests/lock_test.py -setenv = - {[testenv]setenv} - [testenv:py27-oldest] commands = {[testenv]commands} From 43f2bfd6f18b9ab1375b27fc0fd9bf73be64d8ac Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 Jul 2018 09:17:03 -0700 Subject: [PATCH 04/19] Advertise our packages work on Python 3.7. (#6183) --- acme/setup.py | 1 + certbot-apache/setup.py | 1 + certbot-compatibility-test/setup.py | 1 + certbot-dns-cloudflare/setup.py | 1 + certbot-dns-cloudxns/setup.py | 1 + certbot-dns-digitalocean/setup.py | 1 + certbot-dns-dnsimple/setup.py | 1 + certbot-dns-dnsmadeeasy/setup.py | 1 + certbot-dns-google/setup.py | 1 + certbot-dns-luadns/setup.py | 1 + certbot-dns-nsone/setup.py | 1 + certbot-dns-rfc2136/setup.py | 1 + certbot-dns-route53/setup.py | 1 + certbot-nginx/setup.py | 1 + certbot-postfix/setup.py | 1 + letshelp-certbot/setup.py | 1 + setup.py | 1 + 17 files changed, 17 insertions(+) diff --git a/acme/setup.py b/acme/setup.py index ecafac61a..8332febc1 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -68,6 +68,7 @@ setup( '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', ], diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index ffaa6a863..aca7f3bce 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -43,6 +43,7 @@ setup( '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', diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index bf86df5da..34e39ec5e 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -46,6 +46,7 @@ setup( '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', ], diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 055a8cffc..9cf26aa52 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -42,6 +42,7 @@ setup( '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', diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 2c0c074be..8077fe8f5 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -42,6 +42,7 @@ setup( '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', diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index bd5f2ff26..5877d2d0d 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -43,6 +43,7 @@ setup( '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', diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 581c478b8..8c00e6d58 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -42,6 +42,7 @@ setup( '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', diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index c5d310d71..34772c422 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -42,6 +42,7 @@ setup( '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', diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 01d979c2b..ad56e58be 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -47,6 +47,7 @@ setup( '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', diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index 44f67c0a7..796f7a489 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -42,6 +42,7 @@ setup( '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', diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 87a7da4cc..d87470c3a 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -42,6 +42,7 @@ setup( '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', diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index c1fffc4a2..bbfbcbba1 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -42,6 +42,7 @@ setup( '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', diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index c8806e862..8836dc6d8 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -36,6 +36,7 @@ setup( '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', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index b486b2778..09192a956 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -43,6 +43,7 @@ setup( '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', diff --git a/certbot-postfix/setup.py b/certbot-postfix/setup.py index 4c53477d2..0ff2908df 100644 --- a/certbot-postfix/setup.py +++ b/certbot-postfix/setup.py @@ -40,6 +40,7 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Communications :: Email :: Mail Transport Agents', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/letshelp-certbot/setup.py b/letshelp-certbot/setup.py index 28ce0e962..3e9e31725 100644 --- a/letshelp-certbot/setup.py +++ b/letshelp-certbot/setup.py @@ -35,6 +35,7 @@ setup( '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', diff --git a/setup.py b/setup.py index 9ef9ec0d2..5a68ff40e 100644 --- a/setup.py +++ b/setup.py @@ -99,6 +99,7 @@ setup( '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', From 83f7e72fefb8d9087a5ad488153a644e1b905572 Mon Sep 17 00:00:00 2001 From: ohemorange Date: Tue, 10 Jul 2018 13:03:25 -0700 Subject: [PATCH 05/19] Update and delete registration with account reuse (#6174) * find the correct url when deactivating an acmev1 account on the acmev2 endpoint * set regr in ClientNetwork.account after deactivating on the server * update self.net.account * move logic into update_registration * return methods to their original order to please git * factor out common code * update test_fowarding to use a method that still gets forwarded * add acme module test coverage * pragma no cover on correct line * use previous regr uri * strip unnecessary items from regr before saving * add explanation to main.py * add extra check to client_test.py * use empty dict instead of empty string to indicate lack of body that we save to disk --- acme/acme/client.py | 26 +++++++++++++++++++++++++- acme/acme/client_test.py | 22 +++++++++++++++++++++- acme/acme/messages.py | 1 + certbot/account.py | 9 ++++++--- certbot/main.py | 5 +++++ 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index bdc07fb1c..a0bfe460d 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -50,7 +50,6 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes :ivar .ClientNetwork net: Client network. :ivar int acme_version: ACME protocol version. 1 or 2. """ - def __init__(self, directory, net, acme_version): """Initialize. @@ -588,6 +587,30 @@ class ClientV2(ClientBase): self.net.account = regr return regr + def update_registration(self, regr, update=None): + """Update registration. + + :param messages.RegistrationResource regr: Registration Resource. + :param messages.Registration update: Updated body of the + resource. If not provided, body will be taken from `regr`. + + :returns: Updated Registration Resource. + :rtype: `.RegistrationResource` + + """ + # https://github.com/certbot/certbot/issues/6155 + new_regr = self._get_v2_account(regr) + return super(ClientV2, self).update_registration(new_regr, update) + + def _get_v2_account(self, regr): + self.net.account = None + only_existing_reg = regr.body.update(only_return_existing=True) + response = self._post(self.directory['newAccount'], only_existing_reg) + updated_uri = response.headers['Location'] + new_regr = regr.update(uri=updated_uri) + self.net.account = new_regr + return new_regr + def new_order(self, csr_pem): """Request a new Order object from the server. @@ -910,6 +933,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes if acme_version == 2: kwargs["url"] = url # newAccount and revokeCert work without the kid + # newAccount must not have kid if self.account is not None: kwargs["kid"] = self.account["uri"] kwargs["key"] = self.key diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index f3018ed81..cd31c4ac3 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -139,7 +139,7 @@ class BackwardsCompatibleClientV2Test(ClientTestBase): client = self._init() self.assertEqual(client.directory, client.client.directory) self.assertEqual(client.key, KEY) - self.assertEqual(client.update_registration, client.client.update_registration) + self.assertEqual(client.deactivate_registration, client.client.deactivate_registration) self.assertRaises(AttributeError, client.__getattr__, 'nonexistent') self.assertRaises(AttributeError, client.__getattr__, 'new_account_and_tos') self.assertRaises(AttributeError, client.__getattr__, 'new_account') @@ -270,6 +270,13 @@ class BackwardsCompatibleClientV2Test(ClientTestBase): client.revoke(messages_test.CERT, self.rsn) mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) + def test_update_registration(self): + self.response.json.return_value = DIRECTORY_V1.to_json() + with mock.patch('acme.client.Client') as mock_client: + client = self._init() + client.update_registration(mock.sentinel.regr, None) + mock_client().update_registration.assert_called_once_with(mock.sentinel.regr, None) + class ClientTest(ClientTestBase): """Tests for acme.client.Client.""" @@ -789,6 +796,19 @@ class ClientV2Test(ClientTestBase): self.net.post.assert_called_once_with( self.directory["revokeCert"], mock.ANY, acme_version=2) + def test_update_registration(self): + # "Instance of 'Field' has no to_json/update member" bug: + # pylint: disable=no-member + self.response.headers['Location'] = self.regr.uri + self.response.json.return_value = self.regr.body.to_json() + self.assertEqual(self.regr, self.client.update_registration(self.regr)) + self.assertNotEqual(self.client.net.account, None) + self.assertEqual(self.client.net.post.call_count, 2) + self.assertTrue(DIRECTORY_V2.newAccount in self.net.post.call_args_list[0][0]) + + self.response.json.return_value = self.regr.body.update( + contact=()).to_json() + class MockJSONDeSerializable(jose.JSONDeSerializable): # pylint: disable=missing-docstring diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 827a4dd11..5be458580 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -274,6 +274,7 @@ class Registration(ResourceBody): agreement = jose.Field('agreement', omitempty=True) status = jose.Field('status', omitempty=True) terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True) + only_return_existing = jose.Field('onlyReturnExisting', omitempty=True) phone_prefix = 'tel:' email_prefix = 'mailto:' diff --git a/certbot/account.py b/certbot/account.py index 5e9455048..2f261f759 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -270,9 +270,12 @@ class AccountFileStorage(interfaces.AccountStorage): if hasattr(acme.directory, "new-authz"): regr = RegistrationResourceWithNewAuthzrURI( new_authzr_uri=acme.directory.new_authz, - body=regr.body, - uri=regr.uri, - terms_of_service=regr.terms_of_service) + body={}, + uri=regr.uri) + else: + regr = messages.RegistrationResource( + body={}, + uri=regr.uri) regr_file.write(regr.json_dumps()) if not regr_only: with util.safe_open(self._key_path(account_dir_path), diff --git a/certbot/main.py b/certbot/main.py index 556722104..2c7ded3e2 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -735,8 +735,13 @@ def register(config, unused_plugins): cb_client = client.Client(config, acc, None, None, acme=acme) # We rely on an exception to interrupt this process if it didn't work. acc_contacts = ['mailto:' + email for email in config.email.split(',')] + prev_regr_uri = acc.regr.uri acc.regr = cb_client.acme.update_registration(acc.regr.update( body=acc.regr.body.update(contact=acc_contacts))) + # A v1 account being used as a v2 account will result in changing the uri to + # the v2 uri. Since it's the same object on disk, put it back to the v1 uri + # so that we can also continue to use the account object with acmev1. + acc.regr = acc.regr.update(uri=prev_regr_uri) account_storage.save_regr(acc, cb_client.acme) eff.handle_subscription(config) add_msg("Your e-mail address was updated to {0}.".format(config.email)) From 3855cfc08dacccce223eb6c0f3d127143225da2d Mon Sep 17 00:00:00 2001 From: Trinopoty Biswas Date: Wed, 11 Jul 2018 02:21:03 +0530 Subject: [PATCH 06/19] Linode DNS Authenticator (#5302) * Added DNS based authenticator plugin for Linode * Added linode plugin to docs * Added Dockerfile * Added .gitignore and readthedocs.org.requirements.txt * Updated default_propagation_seconds * Updated according to changes requested * Bump version to 0.26.0 * Advertise our packages work on Python 3.7. --- certbot-dns-linode/Dockerfile | 5 + certbot-dns-linode/LICENSE.txt | 190 ++++++++++++++++++ certbot-dns-linode/MANIFEST.in | 3 + certbot-dns-linode/README.rst | 1 + .../certbot_dns_linode/__init__.py | 86 ++++++++ .../certbot_dns_linode/dns_linode.py | 72 +++++++ .../certbot_dns_linode/dns_linode_test.py | 47 +++++ certbot-dns-linode/docs/.gitignore | 1 + certbot-dns-linode/docs/Makefile | 20 ++ certbot-dns-linode/docs/api.rst | 8 + certbot-dns-linode/docs/api/dns_linode.rst | 5 + certbot-dns-linode/docs/conf.py | 180 +++++++++++++++++ certbot-dns-linode/docs/index.rst | 28 +++ certbot-dns-linode/docs/make.bat | 36 ++++ .../local-oldest-requirements.txt | 2 + .../readthedocs.org.requirements.txt | 12 ++ certbot-dns-linode/setup.cfg | 2 + certbot-dns-linode/setup.py | 66 ++++++ certbot/cli.py | 4 + certbot/constants.py | 1 + certbot/plugins/disco.py | 1 + certbot/plugins/selection.py | 4 +- docs/packaging.rst | 2 + docs/using.rst | 1 + tools/release.sh | 2 +- tools/venv.sh | 1 + tools/venv3.sh | 1 + tox.cover.sh | 4 +- tox.ini | 2 + 29 files changed, 784 insertions(+), 3 deletions(-) create mode 100644 certbot-dns-linode/Dockerfile create mode 100644 certbot-dns-linode/LICENSE.txt create mode 100644 certbot-dns-linode/MANIFEST.in create mode 100644 certbot-dns-linode/README.rst create mode 100644 certbot-dns-linode/certbot_dns_linode/__init__.py create mode 100644 certbot-dns-linode/certbot_dns_linode/dns_linode.py create mode 100644 certbot-dns-linode/certbot_dns_linode/dns_linode_test.py create mode 100644 certbot-dns-linode/docs/.gitignore create mode 100644 certbot-dns-linode/docs/Makefile create mode 100644 certbot-dns-linode/docs/api.rst create mode 100644 certbot-dns-linode/docs/api/dns_linode.rst create mode 100644 certbot-dns-linode/docs/conf.py create mode 100644 certbot-dns-linode/docs/index.rst create mode 100644 certbot-dns-linode/docs/make.bat create mode 100644 certbot-dns-linode/local-oldest-requirements.txt create mode 100644 certbot-dns-linode/readthedocs.org.requirements.txt create mode 100644 certbot-dns-linode/setup.cfg create mode 100644 certbot-dns-linode/setup.py diff --git a/certbot-dns-linode/Dockerfile b/certbot-dns-linode/Dockerfile new file mode 100644 index 000000000..2e237b521 --- /dev/null +++ b/certbot-dns-linode/Dockerfile @@ -0,0 +1,5 @@ +FROM certbot/certbot + +COPY . src/certbot-dns-linode + +RUN pip install --no-cache-dir --editable src/certbot-dns-linode diff --git a/certbot-dns-linode/LICENSE.txt b/certbot-dns-linode/LICENSE.txt new file mode 100644 index 000000000..981c46c9f --- /dev/null +++ b/certbot-dns-linode/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/certbot-dns-linode/MANIFEST.in b/certbot-dns-linode/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-linode/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-linode/README.rst b/certbot-dns-linode/README.rst new file mode 100644 index 000000000..69e1fa056 --- /dev/null +++ b/certbot-dns-linode/README.rst @@ -0,0 +1 @@ +Linode DNS Authenticator plugin for Certbot diff --git a/certbot-dns-linode/certbot_dns_linode/__init__.py b/certbot-dns-linode/certbot_dns_linode/__init__.py new file mode 100644 index 000000000..aaed61450 --- /dev/null +++ b/certbot-dns-linode/certbot_dns_linode/__init__.py @@ -0,0 +1,86 @@ +""" +The `~certbot_dns_linode.dns_linode` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the Linode API. + + +Named Arguments +--------------- + +========================================== =================================== +``--dns-linode-credentials`` Linode credentials_ INI file. + (Required) +``--dns-linode-propagation-seconds`` The number of seconds to wait for + DNS to propagate before asking the + ACME server to verify the DNS + record. + (Default: 960) +========================================== =================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing Linode API +credentials, obtained from your Linode account's `Applications & API +Tokens page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # Linode API credentials used by Certbot + dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64 + +The path to this file can be provided interactively or using the +``--dns-linode-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 the password to your + Linode account. 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-linode \\ + --dns-linode-credentials ~/.secrets/certbot/linode.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-linode \\ + --dns-linode-credentials ~/.secrets/certbot/linode.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-linode \\ + --dns-linode-credentials ~/.secrets/certbot/linode.ini \\ + --dns-linode-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-linode/certbot_dns_linode/dns_linode.py b/certbot-dns-linode/certbot_dns_linode/dns_linode.py new file mode 100644 index 000000000..323c0810a --- /dev/null +++ b/certbot-dns-linode/certbot_dns_linode/dns_linode.py @@ -0,0 +1,72 @@ +"""DNS Authenticator for Linode.""" +import logging + +import zope.interface +from lexicon.providers import linode + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common +from certbot.plugins import dns_common_lexicon + +logger = logging.getLogger(__name__) + +API_KEY_URL = 'https://manager.linode.com/profile/api' + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for Linode + + This Authenticator uses the Linode API to fulfill a dns-01 challenge. + """ + + description = 'Obtain certs using a DNS TXT record (if you are using Linode 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, default_propagation_seconds=960) + add('credentials', help='Linode 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 Linode API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'Linode credentials INI file', + { + 'key': 'API key for Linode account, obtained from {0}'.format(API_KEY_URL) + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_linode_client().add_txt_record(domain, validation_name, validation) + + def _cleanup(self, domain, validation_name, validation): + self._get_linode_client().del_txt_record(domain, validation_name, validation) + + def _get_linode_client(self): + return _LinodeLexiconClient(self.credentials.conf('key')) + +class _LinodeLexiconClient(dns_common_lexicon.LexiconClient): + """ + Encapsulates all communication with the Linode API. + """ + + def __init__(self, api_key): + super(_LinodeLexiconClient, self).__init__() + self.provider = linode.Provider({ + 'auth_token': api_key + }) + + def _handle_general_error(self, e, domain_name): + if not str(e).startswith('Domain not found'): + return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}' + .format(domain_name, e)) + diff --git a/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py b/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py new file mode 100644 index 000000000..2a0ee49f7 --- /dev/null +++ b/certbot-dns-linode/certbot_dns_linode/dns_linode_test.py @@ -0,0 +1,47 @@ +"""Tests for certbot_dns_linode.dns_linode.""" + +import os +import unittest + +import mock + +from certbot.plugins import dns_test_common +from certbot.plugins import dns_test_common_lexicon +from certbot.tests import util as test_util + +TOKEN = 'a-token' + +class AuthenticatorTest(test_util.TempDirTestCase, + dns_test_common_lexicon.BaseLexiconAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_linode.dns_linode import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write({"linode_key": TOKEN}, path) + + self.config = mock.MagicMock(linode_credentials=path, + linode_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "linode") + + self.mock_client = mock.MagicMock() + # _get_linode_client | pylint: disable=protected-access + self.auth._get_linode_client = mock.MagicMock(return_value=self.mock_client) + +class LinodeLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + + DOMAIN_NOT_FOUND = Exception('Domain not found') + + def setUp(self): + from certbot_dns_linode.dns_linode import _LinodeLexiconClient + + self.client = _LinodeLexiconClient(TOKEN) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-linode/docs/.gitignore b/certbot-dns-linode/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-linode/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-linode/docs/Makefile b/certbot-dns-linode/docs/Makefile new file mode 100644 index 000000000..bcfbfd5b1 --- /dev/null +++ b/certbot-dns-linode/docs/Makefile @@ -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-linode +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) diff --git a/certbot-dns-linode/docs/api.rst b/certbot-dns-linode/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-linode/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-linode/docs/api/dns_linode.rst b/certbot-dns-linode/docs/api/dns_linode.rst new file mode 100644 index 000000000..6380b3eba --- /dev/null +++ b/certbot-dns-linode/docs/api/dns_linode.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_linode.dns_linode` +------------------------------------------------ + +.. automodule:: certbot_dns_linode.dns_linode + :members: diff --git a/certbot-dns-linode/docs/conf.py b/certbot-dns-linode/docs/conf.py new file mode 100644 index 000000000..1fb721400 --- /dev/null +++ b/certbot-dns-linode/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-linode documentation build configuration file, created by +# sphinx-quickstart on Wed May 10 10:52:06 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-linode' +copyright = u'2017, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-linodedoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-linode.tex', u'certbot-dns-linode Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-linode', u'certbot-dns-linode Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-linode', u'certbot-dns-linode Documentation', + author, 'certbot-dns-linode', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-linode/docs/index.rst b/certbot-dns-linode/docs/index.rst new file mode 100644 index 000000000..dd430554b --- /dev/null +++ b/certbot-dns-linode/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-linode documentation master file, created by + sphinx-quickstart on Wed May 10 10:52:06 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-linode's documentation! +==================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_linode + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-linode/docs/make.bat b/certbot-dns-linode/docs/make.bat new file mode 100644 index 000000000..1f2a6867f --- /dev/null +++ b/certbot-dns-linode/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-linode + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-linode/local-oldest-requirements.txt b/certbot-dns-linode/local-oldest-requirements.txt new file mode 100644 index 000000000..8368d266e --- /dev/null +++ b/certbot-dns-linode/local-oldest-requirements.txt @@ -0,0 +1,2 @@ +acme[dev]==0.21.1 +certbot[dev]==0.21.1 diff --git a/certbot-dns-linode/readthedocs.org.requirements.txt b/certbot-dns-linode/readthedocs.org.requirements.txt new file mode 100644 index 000000000..47449454f --- /dev/null +++ b/certbot-dns-linode/readthedocs.org.requirements.txt @@ -0,0 +1,12 @@ +# readthedocs.org gives no way to change the install command to "pip +# install -e .[docs]" (that would in turn install documentation +# dependencies), but it allows to specify a requirements.txt file at +# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259) + +# Although ReadTheDocs certainly doesn't need to install the project +# in --editable mode (-e), just "pip install .[docs]" does not work as +# expected and "pip install -e .[docs]" must be used instead + +-e acme +-e . +-e certbot-dns-linode[docs] diff --git a/certbot-dns-linode/setup.cfg b/certbot-dns-linode/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-linode/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py new file mode 100644 index 000000000..2abd19b68 --- /dev/null +++ b/certbot-dns-linode/setup.py @@ -0,0 +1,66 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + +version = '0.26.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme>=0.21.1', + 'certbot>=0.21.1', + 'dns-lexicon>=2.2.1', + 'mock', + 'setuptools', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-linode', + version=version, + description="Linode 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, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-linode = certbot_dns_linode.dns_linode:Authenticator', + ], + }, + test_suite='certbot_dns_linode', +) diff --git a/certbot/cli.py b/certbot/cli.py index 5c4313ea4..244daf546 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1419,6 +1419,10 @@ def _plugins_parsing(helpful, plugins): default=flag_default("dns_google"), help=("Obtain certificates using a DNS TXT record (if you are " "using Google Cloud DNS).")) + helpful.add(["plugins", "certonly"], "--dns-linode", action="store_true", + default=flag_default("dns_linode"), + help=("Obtain certificates using a DNS TXT record (if you are " + "using Linode for DNS).")) helpful.add(["plugins", "certonly"], "--dns-luadns", action="store_true", default=flag_default("dns_luadns"), help=("Obtain certificates using a DNS TXT record (if you are " diff --git a/certbot/constants.py b/certbot/constants.py index d31faa71c..750db83b7 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -105,6 +105,7 @@ CLI_DEFAULTS = dict( dns_dnsimple=False, dns_dnsmadeeasy=False, dns_google=False, + dns_linode=False, dns_luadns=False, dns_nsone=False, dns_rfc2136=False, diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index bf9f60e3b..8672ba0ab 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -31,6 +31,7 @@ class PluginEntryPoint(object): "certbot-dns-dnsimple", "certbot-dns-dnsmadeeasy", "certbot-dns-google", + "certbot-dns-linode", "certbot-dns-luadns", "certbot-dns-nsone", "certbot-dns-rfc2136", diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 95f123a46..ef98e9cd8 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -165,7 +165,7 @@ def choose_plugin(prepared, question): noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-google", - "dns-luadns", "dns-nsone", "dns-rfc2136", "dns-route53"] + "dns-linode", "dns-luadns", "dns-nsone", "dns-rfc2136", "dns-route53"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -290,6 +290,8 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches req_auth = set_configurator(req_auth, "dns-dnsmadeeasy") if config.dns_google: req_auth = set_configurator(req_auth, "dns-google") + if config.dns_linode: + req_auth = set_configurator(req_auth, "dns-linode") if config.dns_luadns: req_auth = set_configurator(req_auth, "dns-luadns") if config.dns_nsone: diff --git a/docs/packaging.rst b/docs/packaging.rst index 3d58ea92e..dc75e34b0 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -17,6 +17,7 @@ We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.python.org/pypi/certbot-dns-dnsimple - https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy - https://pypi.python.org/pypi/certbot-dns-google +- https://pypi.python.org/pypi/certbot-dns-linode - https://pypi.python.org/pypi/certbot-dns-luadns - https://pypi.python.org/pypi/certbot-dns-nsone - https://pypi.python.org/pypi/certbot-dns-rfc2136 @@ -63,6 +64,7 @@ From our official releases: - https://www.archlinux.org/packages/community/any/certbot-dns-dnsimple - https://www.archlinux.org/packages/community/any/certbot-dns-dnsmadeeasy - https://www.archlinux.org/packages/community/any/certbot-dns-google +- https://www.archlinux.org/packages/community/any/certbot-dns-linode - https://www.archlinux.org/packages/community/any/certbot-dns-luadns - https://www.archlinux.org/packages/community/any/certbot-dns-nsone - https://www.archlinux.org/packages/community/any/certbot-dns-rfc2136 diff --git a/docs/using.rst b/docs/using.rst index 46599a06e..50c27d45e 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -203,6 +203,7 @@ Once installed, you can find documentation on how to use each plugin at: * `certbot-dns-dnsimple `_ * `certbot-dns-dnsmadeeasy `_ * `certbot-dns-google `_ +* `certbot-dns-linode `_ * `certbot-dns-luadns `_ * `certbot-dns-nsone `_ * `certbot-dns-rfc2136 `_ diff --git a/tools/release.sh b/tools/release.sh index 02c7ccca5..1286a7b45 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,7 +46,7 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53" +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53" # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" diff --git a/tools/venv.sh b/tools/venv.sh index a623ec529..177bb2714 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -20,6 +20,7 @@ fi -e certbot-dns-dnsimple \ -e certbot-dns-dnsmadeeasy \ -e certbot-dns-google \ + -e certbot-dns-linode \ -e certbot-dns-luadns \ -e certbot-dns-nsone \ -e certbot-dns-rfc2136 \ diff --git a/tools/venv3.sh b/tools/venv3.sh index 602118004..f18f05374 100755 --- a/tools/venv3.sh +++ b/tools/venv3.sh @@ -19,6 +19,7 @@ fi -e certbot-dns-dnsimple \ -e certbot-dns-dnsmadeeasy \ -e certbot-dns-google \ + -e certbot-dns-linode \ -e certbot-dns-luadns \ -e certbot-dns-nsone \ -e certbot-dns-route53 \ diff --git a/tox.cover.sh b/tox.cover.sh index 4167699d6..421771d08 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx certbot_postfix letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_linode certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx certbot_postfix letshelp_certbot" else pkgs="$@" fi @@ -33,6 +33,8 @@ cover () { min=99 elif [ "$1" = "certbot_dns_google" ]; then min=99 + elif [ "$1" = "certbot_dns_linode" ]; then + min=98 elif [ "$1" = "certbot_dns_luadns" ]; then min=98 elif [ "$1" = "certbot_dns_nsone" ]; then diff --git a/tox.ini b/tox.ini index ef71b52be..c05627fc1 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,7 @@ dns_packages = certbot-dns-dnsimple \ certbot-dns-dnsmadeeasy \ certbot-dns-google \ + certbot-dns-linode \ certbot-dns-luadns \ certbot-dns-nsone \ certbot-dns-rfc2136 \ @@ -46,6 +47,7 @@ source_paths = certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy certbot-dns-google/certbot_dns_google + certbot-dns-linode/certbot_dns_linode certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone certbot-dns-rfc2136/certbot_dns_rfc2136 From 0672e63176f51b30b5faaa089e425bbe4aa28dd9 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 10 Jul 2018 13:52:58 -0700 Subject: [PATCH 07/19] Remove main components from Alpha. (#6187) acme, certbot, and the Nginx and Apache plugins should no longer be considered alpha-quality. --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-nginx/certbot_nginx/configurator.py | 2 +- certbot-nginx/setup.py | 2 +- docs/cli-help.txt | 2 +- setup.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 8332febc1..e6cfcafc6 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -58,7 +58,7 @@ setup( license='Apache License 2.0', python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index aca7f3bce..cbd434f01 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -31,7 +31,7 @@ setup( license='Apache License 2.0', python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', 'Environment :: Plugins', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index b80d95613..d31a54c17 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -60,7 +60,7 @@ class NginxConfigurator(common.Installer): """ - description = "Nginx Web Server plugin - Alpha" + description = "Nginx Web Server plugin" DEFAULT_LISTEN_PORT = '80' diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 09192a956..b43684feb 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -31,7 +31,7 @@ setup( license='Apache License 2.0', python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', 'Environment :: Plugins', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 594bcfd04..7b8b522c9 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -635,7 +635,7 @@ manual: Automatically allows public IP logging (default: Ask) nginx: - Nginx Web Server plugin - Alpha + Nginx Web Server plugin --nginx-server-root NGINX_SERVER_ROOT Nginx server root directory. (default: /etc/nginx or diff --git a/setup.py b/setup.py index 5a68ff40e..fc87917fb 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ setup( license='Apache License 2.0', python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Console :: Curses', 'Intended Audience :: System Administrators', From 8a5abb620338cd804bd7b09f52865c0e2e7658d3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 10 Jul 2018 14:06:45 -0700 Subject: [PATCH 08/19] Always save server value in renewal conf file (#6189) --- certbot/storage.py | 7 ++++++- certbot/tests/storage_test.py | 22 ++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/certbot/storage.py b/certbot/storage.py index 5b2293bd1..32d6771c2 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -239,10 +239,15 @@ def relevant_values(all_values): :rtype dict: """ - return dict( + rv = dict( (option, value) for option, value in six.iteritems(all_values) if _relevant(option) and cli.option_was_set(option, value)) + # We always save the server value to help with forward compatibility + # and behavioral consistency when versions of Certbot with different + # server defaults are used. + rv["server"] = all_values["server"] + return rv def lineagename_for_filename(config_filename): """Returns the lineagename for a configuration filename. diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index aa6c52ad4..db6aec1de 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -544,14 +544,22 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(os.path.exists(temp_config_file)) def _test_relevant_values_common(self, values): - option = "rsa_key_size" + defaults = dict((option, cli.flag_default(option)) + for option in ("rsa_key_size", "server",)) mock_parser = mock.Mock(args=["--standalone"], verb="certonly", - defaults={option: cli.flag_default(option)}) + defaults=defaults) + + # make a copy to ensure values isn't modified + values = values.copy() + values.setdefault("server", defaults["server"]) + expected_server = values["server"] from certbot.storage import relevant_values with mock.patch("certbot.cli.helpful_parser", mock_parser): - # make a copy to ensure values isn't modified - return relevant_values(values.copy()) + rv = relevant_values(values) + self.assertIn("server", rv) + self.assertEqual(rv.pop("server"), expected_server) + return rv def test_relevant_values(self): """Test that relevant_values() can reject an irrelevant value.""" @@ -589,6 +597,12 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual( self._test_relevant_values_common(values), values) + def test_relevant_values_server(self): + self.assertEqual( + # _test_relevant_values_common handles testing the server + # value and removes it + self._test_relevant_values_common({"server": "example.org"}), {}) + @mock.patch("certbot.storage.relevant_values") def test_new_lineage(self, mock_rv): """Test for new_lineage() class method.""" From 9314911135ad2ef36dd561eafe4a5e23c4c422ad Mon Sep 17 00:00:00 2001 From: chibiegg Date: Wed, 11 Jul 2018 06:30:37 +0900 Subject: [PATCH 09/19] Sakura Cloud DNS Authenticator (#5701) Implement an Authenticator which can fulfill a dns-01 challenge using the Sakura Cloud DNS API. Applicable only for domains using Sakura Cloud for DNS. Testing Done: * `tox -e py27` * `tox -e lint` * Manual testing: * Used `certbot certonly --dns-sakuracloud -d`, specifying a credentials file as a command line argument. Verified that a certificate was successfully obtained without user interaction. * Negative testing: * Path to non-existent credentials file. * Credentials file with unsafe permissions (644). * Domain name not registered to Sakura Cloud account. --- certbot-dns-sakuracloud/Dockerfile | 5 + certbot-dns-sakuracloud/LICENSE.txt | 190 ++++++++++++++++++ certbot-dns-sakuracloud/MANIFEST.in | 3 + certbot-dns-sakuracloud/README.rst | 1 + .../certbot_dns_sakuracloud/__init__.py | 86 ++++++++ .../dns_sakuracloud.py | 87 ++++++++ .../dns_sakuracloud_test.py | 55 +++++ certbot-dns-sakuracloud/docs/.gitignore | 1 + certbot-dns-sakuracloud/docs/Makefile | 20 ++ certbot-dns-sakuracloud/docs/api.rst | 8 + .../docs/api/dns_sakuracloud.rst | 5 + certbot-dns-sakuracloud/docs/conf.py | 180 +++++++++++++++++ certbot-dns-sakuracloud/docs/index.rst | 28 +++ certbot-dns-sakuracloud/docs/make.bat | 36 ++++ .../readthedocs.org.requirements.txt | 12 ++ certbot-dns-sakuracloud/setup.cfg | 2 + certbot-dns-sakuracloud/setup.py | 66 ++++++ certbot/cli.py | 4 + certbot/constants.py | 3 +- certbot/plugins/disco.py | 1 + certbot/plugins/selection.py | 5 +- tools/release.sh | 2 +- tools/venv.sh | 1 + tools/venv3.sh | 1 + tox.cover.sh | 4 +- tox.ini | 4 +- 26 files changed, 805 insertions(+), 5 deletions(-) create mode 100644 certbot-dns-sakuracloud/Dockerfile create mode 100644 certbot-dns-sakuracloud/LICENSE.txt create mode 100644 certbot-dns-sakuracloud/MANIFEST.in create mode 100644 certbot-dns-sakuracloud/README.rst create mode 100644 certbot-dns-sakuracloud/certbot_dns_sakuracloud/__init__.py create mode 100644 certbot-dns-sakuracloud/certbot_dns_sakuracloud/dns_sakuracloud.py create mode 100644 certbot-dns-sakuracloud/certbot_dns_sakuracloud/dns_sakuracloud_test.py create mode 100644 certbot-dns-sakuracloud/docs/.gitignore create mode 100644 certbot-dns-sakuracloud/docs/Makefile create mode 100644 certbot-dns-sakuracloud/docs/api.rst create mode 100644 certbot-dns-sakuracloud/docs/api/dns_sakuracloud.rst create mode 100644 certbot-dns-sakuracloud/docs/conf.py create mode 100644 certbot-dns-sakuracloud/docs/index.rst create mode 100644 certbot-dns-sakuracloud/docs/make.bat create mode 100644 certbot-dns-sakuracloud/readthedocs.org.requirements.txt create mode 100644 certbot-dns-sakuracloud/setup.cfg create mode 100644 certbot-dns-sakuracloud/setup.py diff --git a/certbot-dns-sakuracloud/Dockerfile b/certbot-dns-sakuracloud/Dockerfile new file mode 100644 index 000000000..694773f61 --- /dev/null +++ b/certbot-dns-sakuracloud/Dockerfile @@ -0,0 +1,5 @@ +FROM certbot/certbot + +COPY . src/certbot-dns-sakuracloud + +RUN pip install --no-cache-dir --editable src/certbot-dns-sakuracloud diff --git a/certbot-dns-sakuracloud/LICENSE.txt b/certbot-dns-sakuracloud/LICENSE.txt new file mode 100644 index 000000000..8316b6a0e --- /dev/null +++ b/certbot-dns-sakuracloud/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2018 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/certbot-dns-sakuracloud/MANIFEST.in b/certbot-dns-sakuracloud/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-sakuracloud/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-sakuracloud/README.rst b/certbot-dns-sakuracloud/README.rst new file mode 100644 index 000000000..46a082b9c --- /dev/null +++ b/certbot-dns-sakuracloud/README.rst @@ -0,0 +1 @@ +Sakura Cloud DNS Authenticator plugin for Certbot diff --git a/certbot-dns-sakuracloud/certbot_dns_sakuracloud/__init__.py b/certbot-dns-sakuracloud/certbot_dns_sakuracloud/__init__.py new file mode 100644 index 000000000..f18780c18 --- /dev/null +++ b/certbot-dns-sakuracloud/certbot_dns_sakuracloud/__init__.py @@ -0,0 +1,86 @@ +""" +The `~certbot_dns_sakuracloud.dns_sakuracloud` plugin automates the process of completing +a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently +removing, TXT records using the Sakura Cloud DNS API. + + +Named Arguments +--------------- + +========================================== ====================================== +``--dns-sakuracloud-credentials`` Sakura Cloud credentials_ INI file. + (Required) +``--dns-sakuracloud-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 90) +========================================== ====================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing +Sakura Cloud DNS API credentials, obtained from your Sakura Cloud DNS +`apikey page `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # Sakura Cloud API credentials used by Certbot + dns_sakuracloud_api_token = 00000000-0000-0000-0000-000000000000 + dns_sakuracloud_api_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw + +The path to this file can be provided interactively or using the +``--dns-sakuracloud-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 the password to your + Sakura Cloud account. 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-sakuracloud \\ + --dns-sakuracloud-credentials ~/.secrets/certbot/sakuracloud.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-sakuracloud \\ + --dns-sakuracloud-credentials ~/.secrets/certbot/sakuracloud.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-sakuracloud \\ + --dns-sakuracloud-credentials ~/.secrets/certbot/sakuracloud.ini \\ + --dns-sakuracloud-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-sakuracloud/certbot_dns_sakuracloud/dns_sakuracloud.py b/certbot-dns-sakuracloud/certbot_dns_sakuracloud/dns_sakuracloud.py new file mode 100644 index 000000000..6f1c74b68 --- /dev/null +++ b/certbot-dns-sakuracloud/certbot_dns_sakuracloud/dns_sakuracloud.py @@ -0,0 +1,87 @@ +"""DNS Authenticator for Sakura Cloud DNS.""" +import logging + +import zope.interface +from lexicon.providers import sakuracloud + +from certbot import interfaces +from certbot.plugins import dns_common +from certbot.plugins import dns_common_lexicon + +logger = logging.getLogger(__name__) + +APIKEY_URL = "https://secure.sakura.ad.jp/cloud/#!/apikey/top/" + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for Sakura Cloud DNS + + This Authenticator uses the Sakura Cloud API to fulfill a dns-01 challenge. + """ + + description = 'Obtain certificates using a DNS TXT record ' + \ + '(if you are using Sakura Cloud 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=90) + add('credentials', help='Sakura Cloud credentials 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 Sakura Cloud API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'Sakura Cloud credentials file', + { + 'api-token': \ + 'API token for Sakura Cloud API obtained from {0}'.format(APIKEY_URL), + 'api-secret': \ + 'API secret for Sakura Cloud API obtained from {0}'.format(APIKEY_URL), + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_sakuracloud_client().add_txt_record( + domain, validation_name, validation) + + def _cleanup(self, domain, validation_name, validation): + self._get_sakuracloud_client().del_txt_record( + domain, validation_name, validation) + + def _get_sakuracloud_client(self): + return _SakuraCloudLexiconClient( + self.credentials.conf('api-token'), + self.credentials.conf('api-secret'), + self.ttl + ) + + +class _SakuraCloudLexiconClient(dns_common_lexicon.LexiconClient): + """ + Encapsulates all communication with the Sakura Cloud via Lexicon. + """ + + def __init__(self, api_token, api_secret, ttl): + super(_SakuraCloudLexiconClient, self).__init__() + + self.provider = sakuracloud.Provider({ + 'auth_token': api_token, + 'auth_secret': api_secret, + 'ttl': ttl, + }) + + def _handle_http_error(self, e, domain_name): + if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:')): + return # Expected errors when zone name guess is wrong + return super(_SakuraCloudLexiconClient, self)._handle_http_error(e, domain_name) diff --git a/certbot-dns-sakuracloud/certbot_dns_sakuracloud/dns_sakuracloud_test.py b/certbot-dns-sakuracloud/certbot_dns_sakuracloud/dns_sakuracloud_test.py new file mode 100644 index 000000000..84605d06f --- /dev/null +++ b/certbot-dns-sakuracloud/certbot_dns_sakuracloud/dns_sakuracloud_test.py @@ -0,0 +1,55 @@ +"""Tests for certbot_dns_sakuracloud.dns_sakuracloud.""" + +import os +import unittest + +import mock +from requests.exceptions import HTTPError + +from certbot.plugins import dns_test_common +from certbot.plugins import dns_test_common_lexicon +from certbot.plugins.dns_test_common import DOMAIN +from certbot.tests import util as test_util + +API_TOKEN = '00000000-0000-0000-0000-000000000000' +API_SECRET = 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw' + +class AuthenticatorTest(test_util.TempDirTestCase, + dns_test_common_lexicon.BaseLexiconAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_sakuracloud.dns_sakuracloud import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write( + {"sakuracloud_api_token": API_TOKEN, "sakuracloud_api_secret": API_SECRET}, + path + ) + + self.config = mock.MagicMock(sakuracloud_credentials=path, + sakuracloud_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "sakuracloud") + + self.mock_client = mock.MagicMock() + # _get_sakuracloud_client | pylint: disable=protected-access + self.auth._get_sakuracloud_client = mock.MagicMock(return_value=self.mock_client) + + +class NS1LexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN)) + LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: {0}.'.format(DOMAIN)) + + def setUp(self): + from certbot_dns_sakuracloud.dns_sakuracloud import _SakuraCloudLexiconClient + + self.client = _SakuraCloudLexiconClient(API_TOKEN, API_SECRET, 0) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-sakuracloud/docs/.gitignore b/certbot-dns-sakuracloud/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-sakuracloud/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-sakuracloud/docs/Makefile b/certbot-dns-sakuracloud/docs/Makefile new file mode 100644 index 000000000..c2969dd98 --- /dev/null +++ b/certbot-dns-sakuracloud/docs/Makefile @@ -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-sakuracloud +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) \ No newline at end of file diff --git a/certbot-dns-sakuracloud/docs/api.rst b/certbot-dns-sakuracloud/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-sakuracloud/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-sakuracloud/docs/api/dns_sakuracloud.rst b/certbot-dns-sakuracloud/docs/api/dns_sakuracloud.rst new file mode 100644 index 000000000..74692e15b --- /dev/null +++ b/certbot-dns-sakuracloud/docs/api/dns_sakuracloud.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_sakuracloud.dns_sakuracloud` +---------------------------------------------- + +.. automodule:: certbot_dns_sakuracloud.dns_sakuracloud + :members: diff --git a/certbot-dns-sakuracloud/docs/conf.py b/certbot-dns-sakuracloud/docs/conf.py new file mode 100644 index 000000000..e14fe1d4c --- /dev/null +++ b/certbot-dns-sakuracloud/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-sakuracloud documentation build configuration file, created by +# sphinx-quickstart on Wed May 10 18:30:40 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-sakuracloud' +copyright = u'2018, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-sakuraclouddoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-sakuracloud.tex', u'certbot-dns-sakuracloud Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-sakuracloud', u'certbot-dns-sakuracloud Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-sakuracloud', u'certbot-dns-sakuracloud Documentation', + author, 'certbot-dns-sakuracloud', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-sakuracloud/docs/index.rst b/certbot-dns-sakuracloud/docs/index.rst new file mode 100644 index 000000000..715028591 --- /dev/null +++ b/certbot-dns-sakuracloud/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-sakuracloud documentation master file, created by + sphinx-quickstart on Wed May 10 18:30:40 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-sakuracloud's documentation! +=================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_sakuracloud + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-sakuracloud/docs/make.bat b/certbot-dns-sakuracloud/docs/make.bat new file mode 100644 index 000000000..0d7706bc7 --- /dev/null +++ b/certbot-dns-sakuracloud/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-sakuracloud + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-sakuracloud/readthedocs.org.requirements.txt b/certbot-dns-sakuracloud/readthedocs.org.requirements.txt new file mode 100644 index 000000000..3f46d95ef --- /dev/null +++ b/certbot-dns-sakuracloud/readthedocs.org.requirements.txt @@ -0,0 +1,12 @@ +# readthedocs.org gives no way to change the install command to "pip +# install -e .[docs]" (that would in turn install documentation +# dependencies), but it allows to specify a requirements.txt file at +# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259) + +# Although ReadTheDocs certainly doesn't need to install the project +# in --editable mode (-e), just "pip install .[docs]" does not work as +# expected and "pip install -e .[docs]" must be used instead + +-e acme +-e . +-e certbot-dns-sakuracloud[docs] diff --git a/certbot-dns-sakuracloud/setup.cfg b/certbot-dns-sakuracloud/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-sakuracloud/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py new file mode 100644 index 000000000..dd8903fdd --- /dev/null +++ b/certbot-dns-sakuracloud/setup.py @@ -0,0 +1,66 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.25.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme>=0.21.1', + 'certbot>=0.21.1', + 'dns-lexicon>=2.1.23', + 'mock', + 'setuptools', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-sakuracloud', + version=version, + description="Sakura Cloud 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', + '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, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-sakuracloud = certbot_dns_sakuracloud.dns_sakuracloud:Authenticator', + ], + }, + test_suite='certbot_dns_sakuracloud', +) diff --git a/certbot/cli.py b/certbot/cli.py index 244daf546..c5199dc25 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1438,6 +1438,10 @@ def _plugins_parsing(helpful, plugins): default=flag_default("dns_route53"), help=("Obtain certificates using a DNS TXT record (if you are using Route53 for " "DNS).")) + helpful.add(["plugins", "certonly"], "--dns-sakuracloud", action="store_true", + default=flag_default("dns_sakuracloud"), + help=("Obtain certificates using a DNS TXT record " + "(if you are using Sakura Cloud for DNS).")) # things should not be reorder past/pre this comment: # plugins_group should be displayed in --help before plugin diff --git a/certbot/constants.py b/certbot/constants.py index 750db83b7..2a752beba 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -109,7 +109,8 @@ CLI_DEFAULTS = dict( dns_luadns=False, dns_nsone=False, dns_rfc2136=False, - dns_route53=False + dns_route53=False, + dns_sakuracloud=False ) STAGING_URI = "https://acme-staging-v02.api.letsencrypt.org/directory" diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index 8672ba0ab..d0a20c3ac 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -36,6 +36,7 @@ class PluginEntryPoint(object): "certbot-dns-nsone", "certbot-dns-rfc2136", "certbot-dns-route53", + "certbot-dns-sakuracloud", "certbot-nginx", "certbot-postfix", ] diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index ef98e9cd8..ec0dfbb19 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -165,7 +165,8 @@ def choose_plugin(prepared, question): noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-google", - "dns-linode", "dns-luadns", "dns-nsone", "dns-rfc2136", "dns-route53"] + "dns-linode", "dns-luadns", "dns-nsone", "dns-rfc2136", "dns-route53", + "dns-sakuracloud"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -300,6 +301,8 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches req_auth = set_configurator(req_auth, "dns-rfc2136") if config.dns_route53: req_auth = set_configurator(req_auth, "dns-route53") + if config.dns_sakuracloud: + req_auth = set_configurator(req_auth, "dns-sakuracloud") logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) return req_auth, req_inst diff --git a/tools/release.sh b/tools/release.sh index 1286a7b45..fdd810497 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,7 +46,7 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53" +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud" # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" diff --git a/tools/venv.sh b/tools/venv.sh index 177bb2714..1610d37d3 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -25,6 +25,7 @@ fi -e certbot-dns-nsone \ -e certbot-dns-rfc2136 \ -e certbot-dns-route53 \ + -e certbot-dns-sakuracloud \ -e certbot-nginx \ -e certbot-postfix \ -e letshelp-certbot \ diff --git a/tools/venv3.sh b/tools/venv3.sh index f18f05374..1cf5554b1 100755 --- a/tools/venv3.sh +++ b/tools/venv3.sh @@ -23,6 +23,7 @@ fi -e certbot-dns-luadns \ -e certbot-dns-nsone \ -e certbot-dns-route53 \ + -e certbot-dns-sakuracloud \ -e certbot-nginx \ -e certbot-postfix \ -e letshelp-certbot \ diff --git a/tox.cover.sh b/tox.cover.sh index 421771d08..8c9766d75 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_linode certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx certbot_postfix letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_linode certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_dns_sakuracloud certbot_nginx certbot_postfix letshelp_certbot" else pkgs="$@" fi @@ -43,6 +43,8 @@ cover () { min=99 elif [ "$1" = "certbot_dns_route53" ]; then min=92 + elif [ "$1" = "certbot_dns_sakuracloud" ]; then + min=97 elif [ "$1" = "certbot_nginx" ]; then min=97 elif [ "$1" = "certbot_postfix" ]; then diff --git a/tox.ini b/tox.ini index c05627fc1..9373b3aa7 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,8 @@ dns_packages = certbot-dns-luadns \ certbot-dns-nsone \ certbot-dns-rfc2136 \ - certbot-dns-route53 + certbot-dns-route53 \ + certbot-dns-sakuracloud all_packages = acme[dev] \ .[dev] \ @@ -52,6 +53,7 @@ source_paths = certbot-dns-nsone/certbot_dns_nsone certbot-dns-rfc2136/certbot_dns_rfc2136 certbot-dns-route53/certbot_dns_route53 + certbot-dns-sakuracloud/certbot_dns_sakuracloud certbot-nginx/certbot_nginx certbot-postfix/certbot_postfix letshelp-certbot/letshelp_certbot From 3b39266813e560448c5ea42c9799ee02de0072b0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 10 Jul 2018 14:42:27 -0700 Subject: [PATCH 10/19] Don't use quoted None for plugins in the config (#6195) This stops us from printing messages like: "Could not choose appropriate plugin for updaters: Could not select or initialize the requested installer None." when certbot renew --force-renewal is run with a lineage that doesn't have an installer. * unquote None * Test None values aren't saved in config file. --- certbot/main.py | 2 +- certbot/plugins/selection.py | 4 ++-- certbot/tests/storage_test.py | 10 ++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 2c7ded3e2..2cba8745b 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1064,7 +1064,7 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config """ # For user-agent construction - config.installer = config.authenticator = "None" + config.installer = config.authenticator = None if config.key_path is not None: # revocation by cert key logger.debug("Revoking %s using cert key %s", config.cert_path[0], config.key_path[0]) diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index ec0dfbb19..2fd84986e 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -170,8 +170,8 @@ noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dn def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." - config.authenticator = plugins.find_init(auth).name if auth else "None" - config.installer = plugins.find_init(inst).name if inst else "None" + config.authenticator = plugins.find_init(auth).name if auth else None + config.installer = plugins.find_init(inst).name if inst else None logger.info("Plugins selected: Authenticator %s, Installer %s", config.authenticator, config.installer) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index db6aec1de..53a976f8d 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -545,8 +545,9 @@ class RenewableCertTests(BaseRenewableCertTest): def _test_relevant_values_common(self, values): defaults = dict((option, cli.flag_default(option)) - for option in ("rsa_key_size", "server",)) - mock_parser = mock.Mock(args=["--standalone"], verb="certonly", + for option in ("authenticator", "installer", + "rsa_key_size", "server",)) + mock_parser = mock.Mock(args=[], verb="plugins", defaults=defaults) # make a copy to ensure values isn't modified @@ -588,6 +589,11 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual( self._test_relevant_values_common(values), values) + def test_relevant_values_plugins_none(self): + self.assertEqual( + self._test_relevant_values_common( + {"authenticator": None, "installer": None}), {}) + @mock.patch("certbot.cli.set_by_cli") @mock.patch("certbot.plugins.disco.PluginsRegistry.find_all") def test_relevant_values_namespace(self, mock_find_all, mock_set_by_cli): From 3f6a90882113fc5954106dd29936f6699487ba0a Mon Sep 17 00:00:00 2001 From: chibiegg Date: Wed, 11 Jul 2018 09:36:20 +0900 Subject: [PATCH 11/19] Gehirn Infrastracture Service DNS Authenticator (#5702) Implement an Authenticator which can fulfill a dns-01 challenge using the Gehirn DNS (Gehirn Infrastructure Service) API. Applicable only for domains using Gehirn DNS for DNS. Testing Done: * `tox -e py27` * `tox -e lint` * Manual testing: * Used `certbot certonly --dns-gehirn -d`, specifying a credentials file as a command line argument. Verified that a certificate was successfully obtained without user interaction. * Negative testing: * Path to non-existent credentials file. * Credentials file with unsafe permissions (644). * Domain name not registered to Gehirn DNS account. --- certbot-dns-gehirn/Dockerfile | 5 + certbot-dns-gehirn/LICENSE.txt | 190 ++++++++++++++++++ certbot-dns-gehirn/MANIFEST.in | 3 + certbot-dns-gehirn/README.rst | 1 + .../certbot_dns_gehirn/__init__.py | 88 ++++++++ .../certbot_dns_gehirn/dns_gehirn.py | 84 ++++++++ .../certbot_dns_gehirn/dns_gehirn_test.py | 55 +++++ certbot-dns-gehirn/docs/.gitignore | 1 + certbot-dns-gehirn/docs/Makefile | 20 ++ certbot-dns-gehirn/docs/api.rst | 8 + certbot-dns-gehirn/docs/api/dns_gehirn.rst | 5 + certbot-dns-gehirn/docs/conf.py | 180 +++++++++++++++++ certbot-dns-gehirn/docs/index.rst | 28 +++ certbot-dns-gehirn/docs/make.bat | 36 ++++ .../readthedocs.org.requirements.txt | 12 ++ certbot-dns-gehirn/setup.cfg | 2 + certbot-dns-gehirn/setup.py | 66 ++++++ certbot/cli.py | 4 + certbot/constants.py | 1 + certbot/plugins/disco.py | 1 + certbot/plugins/selection.py | 8 +- tools/release.sh | 2 +- tools/venv.sh | 1 + tools/venv3.sh | 1 + tox.cover.sh | 4 +- tox.ini | 2 + 26 files changed, 803 insertions(+), 5 deletions(-) create mode 100644 certbot-dns-gehirn/Dockerfile create mode 100644 certbot-dns-gehirn/LICENSE.txt create mode 100644 certbot-dns-gehirn/MANIFEST.in create mode 100644 certbot-dns-gehirn/README.rst create mode 100644 certbot-dns-gehirn/certbot_dns_gehirn/__init__.py create mode 100644 certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn.py create mode 100644 certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn_test.py create mode 100644 certbot-dns-gehirn/docs/.gitignore create mode 100644 certbot-dns-gehirn/docs/Makefile create mode 100644 certbot-dns-gehirn/docs/api.rst create mode 100644 certbot-dns-gehirn/docs/api/dns_gehirn.rst create mode 100644 certbot-dns-gehirn/docs/conf.py create mode 100644 certbot-dns-gehirn/docs/index.rst create mode 100644 certbot-dns-gehirn/docs/make.bat create mode 100644 certbot-dns-gehirn/readthedocs.org.requirements.txt create mode 100644 certbot-dns-gehirn/setup.cfg create mode 100644 certbot-dns-gehirn/setup.py diff --git a/certbot-dns-gehirn/Dockerfile b/certbot-dns-gehirn/Dockerfile new file mode 100644 index 000000000..48ad902b5 --- /dev/null +++ b/certbot-dns-gehirn/Dockerfile @@ -0,0 +1,5 @@ +FROM certbot/certbot + +COPY . src/certbot-dns-gehirn + +RUN pip install --no-cache-dir --editable src/certbot-dns-gehirn diff --git a/certbot-dns-gehirn/LICENSE.txt b/certbot-dns-gehirn/LICENSE.txt new file mode 100644 index 000000000..8316b6a0e --- /dev/null +++ b/certbot-dns-gehirn/LICENSE.txt @@ -0,0 +1,190 @@ + Copyright 2018 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/certbot-dns-gehirn/MANIFEST.in b/certbot-dns-gehirn/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-gehirn/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-gehirn/README.rst b/certbot-dns-gehirn/README.rst new file mode 100644 index 000000000..16058eff8 --- /dev/null +++ b/certbot-dns-gehirn/README.rst @@ -0,0 +1 @@ +Gehirn Infrastracture Service DNS Authenticator plugin for Certbot diff --git a/certbot-dns-gehirn/certbot_dns_gehirn/__init__.py b/certbot-dns-gehirn/certbot_dns_gehirn/__init__.py new file mode 100644 index 000000000..db54154ac --- /dev/null +++ b/certbot-dns-gehirn/certbot_dns_gehirn/__init__.py @@ -0,0 +1,88 @@ +""" +The `~certbot_dns_gehirn.dns_gehirn` plugin automates the process of completing +a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently +removing, TXT records using the Gehirn Infrastracture Service DNS API. + + +Named Arguments +--------------- + +======================================== ===================================== +``--dns-gehirn-credentials`` Gehirn Infrastracture Service + credentials_ INI file. + (Required) +``--dns-gehirn-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +======================================== ===================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing +Gehirn Infrastracture Service DNS API credentials, +obtained from your Gehirn Infrastracture Service +`dashboard `_. + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # Gehirn Infrastracture Service API credentials used by Certbot + dns_gehirn_api_token = 00000000-0000-0000-0000-000000000000 + dns_gehirn_api_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw + +The path to this file can be provided interactively or using the +``--dns-gehirn-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 the password to your + Gehirn Infrastracture Service account. 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-gehirn \\ + --dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-gehirn \\ + --dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-gehirn \\ + --dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\ + --dns-gehirn-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn.py b/certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn.py new file mode 100644 index 000000000..50bfce1ae --- /dev/null +++ b/certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn.py @@ -0,0 +1,84 @@ +"""DNS Authenticator for Gehirn Infrastracture Service DNS.""" +import logging + +import zope.interface +from lexicon.providers import gehirn + +from certbot import interfaces +from certbot.plugins import dns_common +from certbot.plugins import dns_common_lexicon + +logger = logging.getLogger(__name__) + +DASHBOARD_URL = "https://gis.gehirn.jp/" + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for Gehirn Infrastracture Service DNS + + This Authenticator uses the Gehirn Infrastracture Service API to fulfill + a dns-01 challenge. + """ + + description = 'Obtain certificates using a DNS TXT record ' + \ + '(if you are using Gehirn Infrastracture Service 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=30) + add('credentials', help='Gehirn Infrastracture Service credentials 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 Gehirn Infrastracture Service API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'Gehirn Infrastracture Service credentials file', + { + 'api-token': 'API token for Gehirn Infrastracture Service ' + \ + 'API obtained from {0}'.format(DASHBOARD_URL), + 'api-secret': 'API secret for Gehirn Infrastracture Service ' + \ + 'API obtained from {0}'.format(DASHBOARD_URL), + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_gehirn_client().add_txt_record(domain, validation_name, validation) + + def _cleanup(self, domain, validation_name, validation): + self._get_gehirn_client().del_txt_record(domain, validation_name, validation) + + def _get_gehirn_client(self): + return _GehirnLexiconClient( + self.credentials.conf('api-token'), + self.credentials.conf('api-secret'), + self.ttl + ) + + +class _GehirnLexiconClient(dns_common_lexicon.LexiconClient): + """ + Encapsulates all communication with the Gehirn Infrastracture Service via Lexicon. + """ + + def __init__(self, api_token, api_secret, ttl): + super(_GehirnLexiconClient, self).__init__() + + self.provider = gehirn.Provider({ + 'auth_token': api_token, + 'auth_secret': api_secret, + 'ttl': ttl, + }) + + def _handle_http_error(self, e, domain_name): + if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:')): + return # Expected errors when zone name guess is wrong + return super(_GehirnLexiconClient, self)._handle_http_error(e, domain_name) diff --git a/certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn_test.py b/certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn_test.py new file mode 100644 index 000000000..b771c103e --- /dev/null +++ b/certbot-dns-gehirn/certbot_dns_gehirn/dns_gehirn_test.py @@ -0,0 +1,55 @@ +"""Tests for certbot_dns_gehirn.dns_gehirn.""" + +import os +import unittest + +import mock +from requests.exceptions import HTTPError + +from certbot.plugins import dns_test_common +from certbot.plugins import dns_test_common_lexicon +from certbot.plugins.dns_test_common import DOMAIN +from certbot.tests import util as test_util + +API_TOKEN = '00000000-0000-0000-0000-000000000000' +API_SECRET = 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw' + +class AuthenticatorTest(test_util.TempDirTestCase, + dns_test_common_lexicon.BaseLexiconAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_gehirn.dns_gehirn import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write( + {"gehirn_api_token": API_TOKEN, "gehirn_api_secret": API_SECRET}, + path + ) + + self.config = mock.MagicMock(gehirn_credentials=path, + gehirn_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "gehirn") + + self.mock_client = mock.MagicMock() + # _get_gehirn_client | pylint: disable=protected-access + self.auth._get_gehirn_client = mock.MagicMock(return_value=self.mock_client) + + +class GehirnLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN)) + LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: {0}.'.format(DOMAIN)) + + def setUp(self): + from certbot_dns_gehirn.dns_gehirn import _GehirnLexiconClient + + self.client = _GehirnLexiconClient(API_TOKEN, API_SECRET, 0) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-gehirn/docs/.gitignore b/certbot-dns-gehirn/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-gehirn/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-gehirn/docs/Makefile b/certbot-dns-gehirn/docs/Makefile new file mode 100644 index 000000000..a363d1b47 --- /dev/null +++ b/certbot-dns-gehirn/docs/Makefile @@ -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-gehirn +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) \ No newline at end of file diff --git a/certbot-dns-gehirn/docs/api.rst b/certbot-dns-gehirn/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-gehirn/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-gehirn/docs/api/dns_gehirn.rst b/certbot-dns-gehirn/docs/api/dns_gehirn.rst new file mode 100644 index 000000000..35a13e9c1 --- /dev/null +++ b/certbot-dns-gehirn/docs/api/dns_gehirn.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_gehirn.dns_gehirn` +------------------------------------ + +.. automodule:: certbot_dns_gehirn.dns_gehirn + :members: diff --git a/certbot-dns-gehirn/docs/conf.py b/certbot-dns-gehirn/docs/conf.py new file mode 100644 index 000000000..a1b2799fb --- /dev/null +++ b/certbot-dns-gehirn/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-gehirn documentation build configuration file, created by +# sphinx-quickstart on Wed May 10 18:30:40 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-gehirn' +copyright = u'2018, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-gehirndoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-gehirn.tex', u'certbot-dns-gehirn Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-gehirn', u'certbot-dns-gehirn Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-gehirn', u'certbot-dns-gehirn Documentation', + author, 'certbot-dns-gehirn', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-gehirn/docs/index.rst b/certbot-dns-gehirn/docs/index.rst new file mode 100644 index 000000000..77546fa89 --- /dev/null +++ b/certbot-dns-gehirn/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-gehirn documentation master file, created by + sphinx-quickstart on Wed May 10 18:30:40 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-gehirn's documentation! +============================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 1 + + api + +.. automodule:: certbot_dns_gehirn + :members: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-gehirn/docs/make.bat b/certbot-dns-gehirn/docs/make.bat new file mode 100644 index 000000000..905d4ee90 --- /dev/null +++ b/certbot-dns-gehirn/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-gehirn + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-gehirn/readthedocs.org.requirements.txt b/certbot-dns-gehirn/readthedocs.org.requirements.txt new file mode 100644 index 000000000..d9f4f9823 --- /dev/null +++ b/certbot-dns-gehirn/readthedocs.org.requirements.txt @@ -0,0 +1,12 @@ +# readthedocs.org gives no way to change the install command to "pip +# install -e .[docs]" (that would in turn install documentation +# dependencies), but it allows to specify a requirements.txt file at +# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259) + +# Although ReadTheDocs certainly doesn't need to install the project +# in --editable mode (-e), just "pip install .[docs]" does not work as +# expected and "pip install -e .[docs]" must be used instead + +-e acme +-e . +-e certbot-dns-gehirn[docs] diff --git a/certbot-dns-gehirn/setup.cfg b/certbot-dns-gehirn/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-gehirn/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py new file mode 100644 index 000000000..cc47da327 --- /dev/null +++ b/certbot-dns-gehirn/setup.py @@ -0,0 +1,66 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.25.0.dev0' + +# Please update tox.ini when modifying dependency version requirements +install_requires = [ + 'acme>=0.21.1', + 'certbot>=0.21.1', + 'dns-lexicon>=2.1.22', + 'mock', + 'setuptools', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-gehirn', + version=version, + description="Gehirn Infrastracture Service 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', + '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, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-gehirn = certbot_dns_gehirn.dns_gehirn:Authenticator', + ], + }, + test_suite='certbot_dns_gehirn', +) diff --git a/certbot/cli.py b/certbot/cli.py index c5199dc25..6d262ed72 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1415,6 +1415,10 @@ def _plugins_parsing(helpful, plugins): default=flag_default("dns_dnsmadeeasy"), help=("Obtain certificates using a DNS TXT record (if you are" "using DNS Made Easy for DNS).")) + helpful.add(["plugins", "certonly"], "--dns-gehirn", action="store_true", + default=flag_default("dns_gehirn"), + help=("Obtain certificates using a DNS TXT record " + "(if you are using Gehirn Infrastracture Service for DNS).")) helpful.add(["plugins", "certonly"], "--dns-google", action="store_true", default=flag_default("dns_google"), help=("Obtain certificates using a DNS TXT record (if you are " diff --git a/certbot/constants.py b/certbot/constants.py index 2a752beba..70249b89b 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -104,6 +104,7 @@ CLI_DEFAULTS = dict( dns_digitalocean=False, dns_dnsimple=False, dns_dnsmadeeasy=False, + dns_gehirn=False, dns_google=False, dns_linode=False, dns_luadns=False, diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index d0a20c3ac..6ed0cf7b7 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -30,6 +30,7 @@ class PluginEntryPoint(object): "certbot-dns-digitalocean", "certbot-dns-dnsimple", "certbot-dns-dnsmadeeasy", + "certbot-dns-gehirn", "certbot-dns-google", "certbot-dns-linode", "certbot-dns-luadns", diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 2fd84986e..0073c99fe 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -164,9 +164,9 @@ def choose_plugin(prepared, question): return None noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", - "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-google", - "dns-linode", "dns-luadns", "dns-nsone", "dns-rfc2136", "dns-route53", - "dns-sakuracloud"] + "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-gehirn", + "dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-rfc2136", + "dns-route53", "dns-sakuracloud"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -289,6 +289,8 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches req_auth = set_configurator(req_auth, "dns-dnsimple") if config.dns_dnsmadeeasy: req_auth = set_configurator(req_auth, "dns-dnsmadeeasy") + if config.dns_gehirn: + req_auth = set_configurator(req_auth, "dns-gehirn") if config.dns_google: req_auth = set_configurator(req_auth, "dns-google") if config.dns_linode: diff --git a/tools/release.sh b/tools/release.sh index fdd810497..0d42bc22a 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,7 +46,7 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud" +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud" # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" diff --git a/tools/venv.sh b/tools/venv.sh index 1610d37d3..159fc16fb 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -19,6 +19,7 @@ fi -e certbot-dns-digitalocean \ -e certbot-dns-dnsimple \ -e certbot-dns-dnsmadeeasy \ + -e certbot-dns-gehirn \ -e certbot-dns-google \ -e certbot-dns-linode \ -e certbot-dns-luadns \ diff --git a/tools/venv3.sh b/tools/venv3.sh index 1cf5554b1..a1489df22 100755 --- a/tools/venv3.sh +++ b/tools/venv3.sh @@ -18,6 +18,7 @@ fi -e certbot-dns-digitalocean \ -e certbot-dns-dnsimple \ -e certbot-dns-dnsmadeeasy \ + -e certbot-dns-gehirn \ -e certbot-dns-google \ -e certbot-dns-linode \ -e certbot-dns-luadns \ diff --git a/tox.cover.sh b/tox.cover.sh index 8c9766d75..cccaf6103 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_linode certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_dns_sakuracloud certbot_nginx certbot_postfix letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_gehirn certbot_dns_google certbot_dns_linode certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_dns_sakuracloud certbot_nginx certbot_postfix letshelp_certbot" else pkgs="$@" fi @@ -31,6 +31,8 @@ cover () { min=98 elif [ "$1" = "certbot_dns_dnsmadeeasy" ]; then min=99 + elif [ "$1" = "certbot_dns_gehirn" ]; then + min=97 elif [ "$1" = "certbot_dns_google" ]; then min=99 elif [ "$1" = "certbot_dns_linode" ]; then diff --git a/tox.ini b/tox.ini index 9373b3aa7..32020cf14 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ dns_packages = certbot-dns-digitalocean \ certbot-dns-dnsimple \ certbot-dns-dnsmadeeasy \ + certbot-dns-gehirn \ certbot-dns-google \ certbot-dns-linode \ certbot-dns-luadns \ @@ -47,6 +48,7 @@ source_paths = certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy + certbot-dns-gehirn/certbot_dns_gehirn certbot-dns-google/certbot_dns_google certbot-dns-linode/certbot_dns_linode certbot-dns-luadns/certbot_dns_luadns From b7113a35eb4bf93dfa12b8f16f44043739ace11d Mon Sep 17 00:00:00 2001 From: ohemorange Date: Tue, 10 Jul 2018 18:48:09 -0700 Subject: [PATCH 12/19] Delete empty directories after deleting an account, including symlinks up and down the chain, as appropriate (#6176) --- certbot/account.py | 38 +++++++++++++++++++++++++++++++++++ certbot/tests/account_test.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/certbot/account.py b/certbot/account.py index 2f261f759..f2ed5cfd5 100644 --- a/certbot/account.py +++ b/certbot/account.py @@ -250,12 +250,50 @@ class AccountFileStorage(interfaces.AccountStorage): :param account_id: id of account which should be deleted """ + # Step 1: remove the account itself account_dir_path = self._account_dir_path(account_id) if not os.path.isdir(account_dir_path): raise errors.AccountNotFound( "Account at %s does not exist" % account_dir_path) shutil.rmtree(account_dir_path) + # Step 2: remove the directory if it's empty, and linked directories + if not os.listdir(self.config.accounts_dir): + self._delete_accounts_dir_for_server_path(self.config.server_path) + + def _delete_accounts_dir_for_server_path(self, server_path): + accounts_dir_path = self.config.accounts_dir_for_server_path(server_path) + + # does an appropriate directory link to me? if so, make sure that's gone + reused_servers = {} + for k in constants.LE_REUSE_SERVERS: + reused_servers[constants.LE_REUSE_SERVERS[k]] = k + + # is there a next one up? call that and be done + if server_path in reused_servers: + next_server_path = reused_servers[server_path] + next_accounts_dir_path = self.config.accounts_dir_for_server_path(next_server_path) + if os.path.islink(next_accounts_dir_path) \ + and os.readlink(next_accounts_dir_path) == accounts_dir_path: + self._delete_accounts_dir_for_server_path(next_server_path) + return + + # if there's not a next one up to delete, then delete me + # and whatever I link to if applicable + if os.path.islink(accounts_dir_path): + # save my info then delete me + target = os.readlink(accounts_dir_path) + os.unlink(accounts_dir_path) + # then delete whatever I linked to, if appropriate + if server_path in constants.LE_REUSE_SERVERS: + prev_server_path = constants.LE_REUSE_SERVERS[server_path] + prev_accounts_dir_path = self.config.accounts_dir_for_server_path(prev_server_path) + if target == prev_accounts_dir_path: + self._delete_accounts_dir_for_server_path(prev_server_path) + else: + # just delete me + os.rmdir(accounts_dir_path) + def _save(self, account, acme, regr_only): account_dir_path = self._account_dir_path(account.id) util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index e7f82a5b8..e0ec3d5f8 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -273,6 +273,40 @@ class AccountFileStorageTest(test_util.ConfigTestCase): def test_delete_no_account(self): self.assertRaises(errors.AccountNotFound, self.storage.delete, self.acc.id) + def _assert_symlinked_account_removed(self): + # create v1 account + self._set_server('https://acme-staging.api.letsencrypt.org/directory') + self.storage.save(self.acc, self.mock_client) + # ensure v2 isn't already linked to it + with mock.patch('certbot.constants.LE_REUSE_SERVERS', {}): + self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory') + self.assertRaises(errors.AccountNotFound, self.storage.load, self.acc.id) + + def _test_delete_folders(self, server_url): + # create symlinked servers + self._set_server('https://acme-staging.api.letsencrypt.org/directory') + self.storage.save(self.acc, self.mock_client) + self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory') + self.storage.find_all() + + # delete starting at given server_url + self._set_server(server_url) + self.storage.delete(self.acc.id) + + # make sure we're gone from both urls + self._set_server('https://acme-staging.api.letsencrypt.org/directory') + self.assertRaises(errors.AccountNotFound, self.storage.load, self.acc.id) + self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory') + self.assertRaises(errors.AccountNotFound, self.storage.load, self.acc.id) + + def test_delete_folders_up(self): + self._test_delete_folders('https://acme-staging.api.letsencrypt.org/directory') + self._assert_symlinked_account_removed() + + def test_delete_folders_down(self): + self._test_delete_folders('https://acme-staging-v02.api.letsencrypt.org/directory') + self._assert_symlinked_account_removed() + if __name__ == "__main__": unittest.main() # pragma: no cover From 148d68b99a86b3ef0a1257af8becf481a075ebe0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 10 Jul 2018 18:48:49 -0700 Subject: [PATCH 13/19] Fix broken fedora links (#6196) * fix broken fedora links * Add packaged plugin links --- docs/packaging.rst | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/packaging.rst b/docs/packaging.rst index dc75e34b0..48ea02ae7 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -84,8 +84,21 @@ Fedora In Fedora 23+. -- https://admin.fedoraproject.org/pkgdb/package/certbot/ -- https://admin.fedoraproject.org/pkgdb/package/python-acme/ +- https://apps.fedoraproject.org/packages/python-acme +- https://apps.fedoraproject.org/packages/certbot +- https://apps.fedoraproject.org/packages/python-certbot-apache +- https://apps.fedoraproject.org/packages/python-certbot-dns-cloudflare +- https://apps.fedoraproject.org/packages/python-certbot-dns-cloudxns +- https://apps.fedoraproject.org/packages/python-certbot-dns-digitalocean +- https://apps.fedoraproject.org/packages/python-certbot-dns-dnsimple +- https://apps.fedoraproject.org/packages/python-certbot-dns-dnsmadeeasy +- https://apps.fedoraproject.org/packages/python-certbot-dns-google +- https://apps.fedoraproject.org/packages/python-certbot-dns-linode +- https://apps.fedoraproject.org/packages/python-certbot-dns-luadns +- https://apps.fedoraproject.org/packages/python-certbot-dns-nsone +- https://apps.fedoraproject.org/packages/python-certbot-dns-rfc2136 +- https://apps.fedoraproject.org/packages/python-certbot-dns-route53 +- https://apps.fedoraproject.org/packages/python-certbot-nginx FreeBSD ------- From a2222d5bdff304f8c55b63986d09d3f155ef6893 Mon Sep 17 00:00:00 2001 From: Nicolas Bachschmidt Date: Wed, 11 Jul 2018 05:52:32 +0200 Subject: [PATCH 14/19] OVH DNS Authenticator (#5423) Implement an Authenticator which can fulfill a dns-01 challenge using the OVH DNS API. Applicable only for domains using OVH DNS. Testing Done: * `tox -e py27` * `tox -e lint` * Manual testing: * Used `certbot certonly --dns-ovh -d`, specifying a credentials file as a command line argument. Verified that a certificate was successfully obtained without user interaction. * Used `certbot certonly --dns-ovh -d`, without specifying a credentials file as a command line argument. Verified that the user was prompted and that a certificate was successfully obtained. * Used `certbot certonly -d`. Verified that the user was prompted for a credentials file after selecting dnsimple interactively and that a certificate was successfully obtained. * Used `certbot renew --force-renewal`. Verified that certificates were renewed without user interaction. * Negative testing: * Path to non-existent credentials file. * Credentials file with unsafe permissions (644). * Path to credentials file with an invalid application key. * Path to credentials file with an invalid application secret. * Path to credentials file with an invalid consumer key. * Path to credentials file with missing properties. * Domain name not registered to OVH account. --- certbot-dns-ovh/Dockerfile | 5 + certbot-dns-ovh/LICENSE.txt | 190 ++++++++++++++++++ certbot-dns-ovh/MANIFEST.in | 3 + certbot-dns-ovh/README.rst | 1 + certbot-dns-ovh/certbot_dns_ovh/__init__.py | 98 +++++++++ certbot-dns-ovh/certbot_dns_ovh/dns_ovh.py | 102 ++++++++++ .../certbot_dns_ovh/dns_ovh_test.py | 62 ++++++ certbot-dns-ovh/docs/.gitignore | 1 + certbot-dns-ovh/docs/Makefile | 20 ++ certbot-dns-ovh/docs/api.rst | 8 + certbot-dns-ovh/docs/api/dns_ovh.rst | 5 + certbot-dns-ovh/docs/conf.py | 180 +++++++++++++++++ certbot-dns-ovh/docs/index.rst | 28 +++ certbot-dns-ovh/docs/make.bat | 36 ++++ certbot-dns-ovh/local-oldest-requirements.txt | 2 + .../readthedocs.org.requirements.txt | 12 ++ certbot-dns-ovh/setup.cfg | 2 + certbot-dns-ovh/setup.py | 69 +++++++ certbot/cli.py | 4 + certbot/constants.py | 1 + certbot/plugins/disco.py | 1 + certbot/plugins/selection.py | 6 +- docs/cli-help.txt | 12 ++ docs/packaging.rst | 2 + docs/using.rst | 1 + tools/release.sh | 2 +- tools/venv.sh | 1 + tools/venv3.sh | 1 + tox.cover.sh | 4 +- tox.ini | 2 + 30 files changed, 857 insertions(+), 4 deletions(-) create mode 100644 certbot-dns-ovh/Dockerfile create mode 100644 certbot-dns-ovh/LICENSE.txt create mode 100644 certbot-dns-ovh/MANIFEST.in create mode 100644 certbot-dns-ovh/README.rst create mode 100644 certbot-dns-ovh/certbot_dns_ovh/__init__.py create mode 100644 certbot-dns-ovh/certbot_dns_ovh/dns_ovh.py create mode 100644 certbot-dns-ovh/certbot_dns_ovh/dns_ovh_test.py create mode 100644 certbot-dns-ovh/docs/.gitignore create mode 100644 certbot-dns-ovh/docs/Makefile create mode 100644 certbot-dns-ovh/docs/api.rst create mode 100644 certbot-dns-ovh/docs/api/dns_ovh.rst create mode 100644 certbot-dns-ovh/docs/conf.py create mode 100644 certbot-dns-ovh/docs/index.rst create mode 100644 certbot-dns-ovh/docs/make.bat create mode 100644 certbot-dns-ovh/local-oldest-requirements.txt create mode 100644 certbot-dns-ovh/readthedocs.org.requirements.txt create mode 100644 certbot-dns-ovh/setup.cfg create mode 100644 certbot-dns-ovh/setup.py diff --git a/certbot-dns-ovh/Dockerfile b/certbot-dns-ovh/Dockerfile new file mode 100644 index 000000000..e8da96d95 --- /dev/null +++ b/certbot-dns-ovh/Dockerfile @@ -0,0 +1,5 @@ +FROM certbot/certbot + +COPY . src/certbot-dns-ovh + +RUN pip install --no-cache-dir --editable src/certbot-dns-ovh diff --git a/certbot-dns-ovh/LICENSE.txt b/certbot-dns-ovh/LICENSE.txt new file mode 100644 index 000000000..981c46c9f --- /dev/null +++ b/certbot-dns-ovh/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/certbot-dns-ovh/MANIFEST.in b/certbot-dns-ovh/MANIFEST.in new file mode 100644 index 000000000..18f018c08 --- /dev/null +++ b/certbot-dns-ovh/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE.txt +include README.rst +recursive-include docs * diff --git a/certbot-dns-ovh/README.rst b/certbot-dns-ovh/README.rst new file mode 100644 index 000000000..05ffe2a16 --- /dev/null +++ b/certbot-dns-ovh/README.rst @@ -0,0 +1 @@ +OVH DNS Authenticator plugin for Certbot diff --git a/certbot-dns-ovh/certbot_dns_ovh/__init__.py b/certbot-dns-ovh/certbot_dns_ovh/__init__.py new file mode 100644 index 000000000..47f8bda9f --- /dev/null +++ b/certbot-dns-ovh/certbot_dns_ovh/__init__.py @@ -0,0 +1,98 @@ +""" +The `~certbot_dns_ovh.dns_ovh` plugin automates the process of +completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and +subsequently removing, TXT records using the OVH API. + + +Named Arguments +--------------- + +=================================== ========================================== +``--dns-ovh-credentials`` OVH credentials_ INI file. + (Required) +``--dns-ovh-propagation-seconds`` The number of seconds to wait for DNS + to propagate before asking the ACME + server to verify the DNS record. + (Default: 30) +=================================== ========================================== + + +Credentials +----------- + +Use of this plugin requires a configuration file containing OVH API +credentials for an account with the following access rules: + +* ``GET /domain/zone/*`` +* ``PUT /domain/zone/*`` +* ``POST /domain/zone/*`` +* ``DELETE /domain/zone/*`` + +These credentials can be obtained there: + +* `OVH Europe `_ (endpoint: ``ovh-eu``) +* `OVH North America `_ (endpoint: + ``ovh-ca``) + +.. code-block:: ini + :name: credentials.ini + :caption: Example credentials file: + + # OVH API credentials used by Certbot + dns_ovh_endpoint = ovh-eu + dns_ovh_application_key = MDAwMDAwMDAwMDAw + dns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw + dns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw + +The path to this file can be provided interactively or using the +``--dns-ovh-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 the password to your + OVH account. 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-ovh \\ + --dns-ovh-credentials ~/.secrets/certbot/ohv.ini \\ + -d example.com + +.. code-block:: bash + :caption: To acquire a single certificate for both ``example.com`` and + ``www.example.com`` + + certbot certonly \\ + --dns-ovh \\ + --dns-ovh-credentials ~/.secrets/certbot/ovh.ini \\ + -d example.com \\ + -d www.example.com + +.. code-block:: bash + :caption: To acquire a certificate for ``example.com``, waiting 60 seconds + for DNS propagation + + certbot certonly \\ + --dns-ovh \\ + --dns-ovh-credentials ~/.secrets/certbot/ovh.ini \\ + --dns-ovh-propagation-seconds 60 \\ + -d example.com + +""" diff --git a/certbot-dns-ovh/certbot_dns_ovh/dns_ovh.py b/certbot-dns-ovh/certbot_dns_ovh/dns_ovh.py new file mode 100644 index 000000000..c4ded7748 --- /dev/null +++ b/certbot-dns-ovh/certbot_dns_ovh/dns_ovh.py @@ -0,0 +1,102 @@ +"""DNS Authenticator for OVH DNS.""" +import logging + +import zope.interface +from lexicon.providers import ovh + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common +from certbot.plugins import dns_common_lexicon + +logger = logging.getLogger(__name__) + +TOKEN_URL = 'https://eu.api.ovh.com/createToken/ or https://ca.api.ovh.com/createToken/' + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for OVH + + This Authenticator uses the OVH API to fulfill a dns-01 challenge. + """ + + description = 'Obtain certificates using a DNS TXT record (if you are using OVH 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=30) + add('credentials', help='OVH 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 OVH API.' + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + 'credentials', + 'OVH credentials INI file', + { + 'endpoint': 'OVH API endpoint (ovh-eu or ovh-ca)', + 'application-key': 'Application key for OVH API, obtained from {0}' + .format(TOKEN_URL), + 'application-secret': 'Application secret for OVH API, obtained from {0}' + .format(TOKEN_URL), + 'consumer-key': 'Consumer key for OVH API, obtained from {0}' + .format(TOKEN_URL), + } + ) + + def _perform(self, domain, validation_name, validation): + self._get_ovh_client().add_txt_record(domain, validation_name, validation) + + def _cleanup(self, domain, validation_name, validation): + self._get_ovh_client().del_txt_record(domain, validation_name, validation) + + def _get_ovh_client(self): + return _OVHLexiconClient( + self.credentials.conf('endpoint'), + self.credentials.conf('application-key'), + self.credentials.conf('application-secret'), + self.credentials.conf('consumer-key'), + self.ttl + ) + + +class _OVHLexiconClient(dns_common_lexicon.LexiconClient): + """ + Encapsulates all communication with the OVH API via Lexicon. + """ + + def __init__(self, endpoint, application_key, application_secret, consumer_key, ttl): + super(_OVHLexiconClient, self).__init__() + + self.provider = ovh.Provider({ + 'auth_entrypoint': endpoint, + 'auth_application_key': application_key, + 'auth_application_secret': application_secret, + 'auth_consumer_key': consumer_key, + 'ttl': ttl, + }) + + def _handle_http_error(self, e, domain_name): + hint = None + if str(e).startswith('400 Client Error:'): + hint = 'Is your Application Secret value correct?' + if str(e).startswith('403 Client Error:'): + hint = 'Are your Application Key and Consumer Key values correct?' + + return errors.PluginError('Error determining zone identifier for {0}: {1}.{2}' + .format(domain_name, e, ' ({0})'.format(hint) if hint else '')) + + def _handle_general_error(self, e, domain_name): + if domain_name in str(e) and str(e).endswith('not found'): + return + + super(_OVHLexiconClient, self)._handle_general_error(e, domain_name) diff --git a/certbot-dns-ovh/certbot_dns_ovh/dns_ovh_test.py b/certbot-dns-ovh/certbot_dns_ovh/dns_ovh_test.py new file mode 100644 index 000000000..f2a10485d --- /dev/null +++ b/certbot-dns-ovh/certbot_dns_ovh/dns_ovh_test.py @@ -0,0 +1,62 @@ +"""Tests for certbot_dns_ovh.dns_ovh.""" + +import os +import unittest + +import mock +from requests.exceptions import HTTPError + +from certbot.plugins import dns_test_common +from certbot.plugins import dns_test_common_lexicon +from certbot.tests import util as test_util + +ENDPOINT = 'ovh-eu' +APPLICATION_KEY = 'foo' +APPLICATION_SECRET = 'bar' +CONSUMER_KEY = 'spam' + + +class AuthenticatorTest(test_util.TempDirTestCase, + dns_test_common_lexicon.BaseLexiconAuthenticatorTest): + + def setUp(self): + super(AuthenticatorTest, self).setUp() + + from certbot_dns_ovh.dns_ovh import Authenticator + + path = os.path.join(self.tempdir, 'file.ini') + credentials = { + "ovh_endpoint": ENDPOINT, + "ovh_application_key": APPLICATION_KEY, + "ovh_application_secret": APPLICATION_SECRET, + "ovh_consumer_key": CONSUMER_KEY, + } + dns_test_common.write(credentials, path) + + self.config = mock.MagicMock(ovh_credentials=path, + ovh_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "ovh") + + self.mock_client = mock.MagicMock() + # _get_ovh_client | pylint: disable=protected-access + self.auth._get_ovh_client = mock.MagicMock(return_value=self.mock_client) + + +class OVHLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): + DOMAIN_NOT_FOUND = Exception('Domain example.com not found') + LOGIN_ERROR = HTTPError('403 Client Error: Forbidden for url: https://eu.api.ovh.com/1.0/...') + + def setUp(self): + from certbot_dns_ovh.dns_ovh import _OVHLexiconClient + + self.client = _OVHLexiconClient( + ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY, 0 + ) + + self.provider_mock = mock.MagicMock() + self.client.provider = self.provider_mock + + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/certbot-dns-ovh/docs/.gitignore b/certbot-dns-ovh/docs/.gitignore new file mode 100644 index 000000000..ba65b13af --- /dev/null +++ b/certbot-dns-ovh/docs/.gitignore @@ -0,0 +1 @@ +/_build/ diff --git a/certbot-dns-ovh/docs/Makefile b/certbot-dns-ovh/docs/Makefile new file mode 100644 index 000000000..38f6a9159 --- /dev/null +++ b/certbot-dns-ovh/docs/Makefile @@ -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-ovh +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) \ No newline at end of file diff --git a/certbot-dns-ovh/docs/api.rst b/certbot-dns-ovh/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/certbot-dns-ovh/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/certbot-dns-ovh/docs/api/dns_ovh.rst b/certbot-dns-ovh/docs/api/dns_ovh.rst new file mode 100644 index 000000000..79863d05f --- /dev/null +++ b/certbot-dns-ovh/docs/api/dns_ovh.rst @@ -0,0 +1,5 @@ +:mod:`certbot_dns_ovh.dns_ovh` +------------------------------ + +.. automodule:: certbot_dns_ovh.dns_ovh + :members: diff --git a/certbot-dns-ovh/docs/conf.py b/certbot-dns-ovh/docs/conf.py new file mode 100644 index 000000000..57194666e --- /dev/null +++ b/certbot-dns-ovh/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# certbot-dns-ovh documentation build configuration file, created by +# sphinx-quickstart on Fri Jan 12 10:14:31 2018. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode'] + +autodoc_member_order = 'bysource' +autodoc_default_flags = ['show-inheritance', 'private-members'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'certbot-dns-ovh' +copyright = u'2018, Certbot Project' +author = u'Certbot Project' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0' +# The full version, including alpha/beta/rc tags. +release = u'0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +default_role = 'py:obj' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'certbot-dns-ovhdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'certbot-dns-ovh.tex', u'certbot-dns-ovh Documentation', + u'Certbot Project', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'certbot-dns-ovh', u'certbot-dns-ovh Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'certbot-dns-ovh', u'certbot-dns-ovh Documentation', + author, 'certbot-dns-ovh', 'One line description of project.', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'certbot': ('https://certbot.eff.org/docs/', None), +} diff --git a/certbot-dns-ovh/docs/index.rst b/certbot-dns-ovh/docs/index.rst new file mode 100644 index 000000000..ad5860289 --- /dev/null +++ b/certbot-dns-ovh/docs/index.rst @@ -0,0 +1,28 @@ +.. certbot-dns-ovh documentation master file, created by + sphinx-quickstart on Fri Jan 12 10:14:31 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to certbot-dns-ovh's documentation! +=========================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. automodule:: certbot_dns_ovh + :members: + +.. toctree:: + :maxdepth: 1 + + api + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/certbot-dns-ovh/docs/make.bat b/certbot-dns-ovh/docs/make.bat new file mode 100644 index 000000000..78f7dd669 --- /dev/null +++ b/certbot-dns-ovh/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=certbot-dns-ovh + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/certbot-dns-ovh/local-oldest-requirements.txt b/certbot-dns-ovh/local-oldest-requirements.txt new file mode 100644 index 000000000..8368d266e --- /dev/null +++ b/certbot-dns-ovh/local-oldest-requirements.txt @@ -0,0 +1,2 @@ +acme[dev]==0.21.1 +certbot[dev]==0.21.1 diff --git a/certbot-dns-ovh/readthedocs.org.requirements.txt b/certbot-dns-ovh/readthedocs.org.requirements.txt new file mode 100644 index 000000000..0780e12a1 --- /dev/null +++ b/certbot-dns-ovh/readthedocs.org.requirements.txt @@ -0,0 +1,12 @@ +# readthedocs.org gives no way to change the install command to "pip +# install -e .[docs]" (that would in turn install documentation +# dependencies), but it allows to specify a requirements.txt file at +# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259) + +# Although ReadTheDocs certainly doesn't need to install the project +# in --editable mode (-e), just "pip install .[docs]" does not work as +# expected and "pip install -e .[docs]" must be used instead + +-e acme +-e . +-e certbot-dns-ovh[docs] diff --git a/certbot-dns-ovh/setup.cfg b/certbot-dns-ovh/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/certbot-dns-ovh/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py new file mode 100644 index 000000000..4e2e664a4 --- /dev/null +++ b/certbot-dns-ovh/setup.py @@ -0,0 +1,69 @@ +import sys + +from setuptools import setup +from setuptools import find_packages + + +version = '0.25.0.dev0' + +# Remember to update local-oldest-requirements.txt when changing the minimum +# acme/certbot version. +install_requires = [ + 'acme>=0.21.1', + 'certbot>=0.21.1', + 'dns-lexicon>=2.2.1', # Support for >1 TXT record per name + 'mock', + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', + 'zope.interface', +] + +docs_extras = [ + 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags + 'sphinx_rtd_theme', +] + +setup( + name='certbot-dns-ovh', + version=version, + description="OVH 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', + '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, + extras_require={ + 'docs': docs_extras, + }, + entry_points={ + 'certbot.plugins': [ + 'dns-ovh = certbot_dns_ovh.dns_ovh:Authenticator', + ], + }, + test_suite='certbot_dns_ovh', +) diff --git a/certbot/cli.py b/certbot/cli.py index 6d262ed72..2c4aa6530 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1435,6 +1435,10 @@ def _plugins_parsing(helpful, plugins): default=flag_default("dns_nsone"), help=("Obtain certificates using a DNS TXT record (if you are " "using NS1 for DNS).")) + helpful.add(["plugins", "certonly"], "--dns-ovh", action="store_true", + default=flag_default("dns_ovh"), + help=("Obtain certificates using a DNS TXT record (if you are " + "using OVH for DNS).")) helpful.add(["plugins", "certonly"], "--dns-rfc2136", action="store_true", default=flag_default("dns_rfc2136"), help="Obtain certificates using a DNS TXT record (if you are using BIND for DNS).") diff --git a/certbot/constants.py b/certbot/constants.py index 70249b89b..46523ce4d 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -109,6 +109,7 @@ CLI_DEFAULTS = dict( dns_linode=False, dns_luadns=False, dns_nsone=False, + dns_ovh=False, dns_rfc2136=False, dns_route53=False, dns_sakuracloud=False diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index 6ed0cf7b7..7be320efc 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -35,6 +35,7 @@ class PluginEntryPoint(object): "certbot-dns-linode", "certbot-dns-luadns", "certbot-dns-nsone", + "certbot-dns-ovh", "certbot-dns-rfc2136", "certbot-dns-route53", "certbot-dns-sakuracloud", diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index 0073c99fe..9c2138247 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -165,8 +165,8 @@ def choose_plugin(prepared, question): noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-gehirn", - "dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-rfc2136", - "dns-route53", "dns-sakuracloud"] + "dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-ovh", + "dns-rfc2136", "dns-route53", "dns-sakuracloud"] def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." @@ -299,6 +299,8 @@ def cli_plugin_requests(config): # pylint: disable=too-many-branches req_auth = set_configurator(req_auth, "dns-luadns") if config.dns_nsone: req_auth = set_configurator(req_auth, "dns-nsone") + if config.dns_ovh: + req_auth = set_configurator(req_auth, "dns-ovh") if config.dns_rfc2136: req_auth = set_configurator(req_auth, "dns-rfc2136") if config.dns_route53: diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 7b8b522c9..8bba718d5 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -454,6 +454,8 @@ plugins: using LuaDNS for DNS). (default: False) --dns-nsone Obtain certificates using a DNS TXT record (if you are using NS1 for DNS). (default: False) + --dns-ovh Obtain certificates using a DNS TXT record (if you are + using OVH for DNS). (default: False) --dns-rfc2136 Obtain certificates using a DNS TXT record (if you are using BIND for DNS). (default: False) --dns-route53 Obtain certificates using a DNS TXT record (if you are @@ -589,6 +591,16 @@ dns-nsone: --dns-nsone-credentials DNS_NSONE_CREDENTIALS NS1 credentials file. (default: None) +dns-ovh: + Obtain certificates using a DNS TXT record (if you are using OVH for DNS). + + --dns-ovh-propagation-seconds DNS_OVH_PROPAGATION_SECONDS + The number of seconds to wait for DNS to propagate + before asking the ACME server to verify the DNS + record. (default: 30) + --dns-ovh-credentials DNS_OVH_CREDENTIALS + OVH credentials file. (default: None) + dns-rfc2136: Obtain certificates using a DNS TXT record (if you are using BIND for DNS). diff --git a/docs/packaging.rst b/docs/packaging.rst index 48ea02ae7..a86b770c5 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -20,6 +20,7 @@ We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.python.org/pypi/certbot-dns-linode - https://pypi.python.org/pypi/certbot-dns-luadns - https://pypi.python.org/pypi/certbot-dns-nsone +- https://pypi.python.org/pypi/certbot-dns-ovh - https://pypi.python.org/pypi/certbot-dns-rfc2136 - https://pypi.python.org/pypi/certbot-dns-route53 @@ -67,6 +68,7 @@ From our official releases: - https://www.archlinux.org/packages/community/any/certbot-dns-linode - https://www.archlinux.org/packages/community/any/certbot-dns-luadns - https://www.archlinux.org/packages/community/any/certbot-dns-nsone +- https://www.archlinux.org/packages/community/any/certbot-dns-ovh - https://www.archlinux.org/packages/community/any/certbot-dns-rfc2136 - https://www.archlinux.org/packages/community/any/certbot-dns-route53 diff --git a/docs/using.rst b/docs/using.rst index 50c27d45e..946c12bc6 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -206,6 +206,7 @@ Once installed, you can find documentation on how to use each plugin at: * `certbot-dns-linode `_ * `certbot-dns-luadns `_ * `certbot-dns-nsone `_ +* `certbot-dns-ovh `_ * `certbot-dns-rfc2136 `_ * `certbot-dns-route53 `_ diff --git a/tools/release.sh b/tools/release.sh index 0d42bc22a..880563b4b 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -46,7 +46,7 @@ PORT=${PORT:-1234} # subpackages to be released (the way developers think about them) SUBPKGS_IN_AUTO_NO_CERTBOT="acme certbot-apache certbot-nginx" -SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud" +SUBPKGS_NOT_IN_AUTO="certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud" # subpackages to be released (the way the script thinks about them) SUBPKGS_IN_AUTO="certbot $SUBPKGS_IN_AUTO_NO_CERTBOT" diff --git a/tools/venv.sh b/tools/venv.sh index 159fc16fb..5692f9ebf 100755 --- a/tools/venv.sh +++ b/tools/venv.sh @@ -24,6 +24,7 @@ fi -e certbot-dns-linode \ -e certbot-dns-luadns \ -e certbot-dns-nsone \ + -e certbot-dns-ovh \ -e certbot-dns-rfc2136 \ -e certbot-dns-route53 \ -e certbot-dns-sakuracloud \ diff --git a/tools/venv3.sh b/tools/venv3.sh index a1489df22..784fc42e8 100755 --- a/tools/venv3.sh +++ b/tools/venv3.sh @@ -23,6 +23,7 @@ fi -e certbot-dns-linode \ -e certbot-dns-luadns \ -e certbot-dns-nsone \ + -e certbot-dns-ovh \ -e certbot-dns-route53 \ -e certbot-dns-sakuracloud \ -e certbot-nginx \ diff --git a/tox.cover.sh b/tox.cover.sh index cccaf6103..c713327c5 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -9,7 +9,7 @@ # -e makes sure we fail fast and don't submit coveralls submit if [ "xxx$1" = "xxx" ]; then - pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_gehirn certbot_dns_google certbot_dns_linode certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_dns_sakuracloud certbot_nginx certbot_postfix letshelp_certbot" + pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_gehirn certbot_dns_google certbot_dns_linode certbot_dns_luadns certbot_dns_nsone certbot_dns_ovh certbot_dns_rfc2136 certbot_dns_route53 certbot_dns_sakuracloud certbot_nginx certbot_postfix letshelp_certbot" else pkgs="$@" fi @@ -41,6 +41,8 @@ cover () { min=98 elif [ "$1" = "certbot_dns_nsone" ]; then min=99 + elif [ "$1" = "certbot_dns_ovh" ]; then + min=97 elif [ "$1" = "certbot_dns_rfc2136" ]; then min=99 elif [ "$1" = "certbot_dns_route53" ]; then diff --git a/tox.ini b/tox.ini index 32020cf14..482c65c36 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ dns_packages = certbot-dns-linode \ certbot-dns-luadns \ certbot-dns-nsone \ + certbot-dns-ovh \ certbot-dns-rfc2136 \ certbot-dns-route53 \ certbot-dns-sakuracloud @@ -53,6 +54,7 @@ source_paths = certbot-dns-linode/certbot_dns_linode certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone + certbot-dns-ovh/certbot_dns_ovh certbot-dns-rfc2136/certbot_dns_rfc2136 certbot-dns-route53/certbot_dns_route53 certbot-dns-sakuracloud/certbot_dns_sakuracloud From c326c021082dede7c3b2bd411cec3aec6dff0ac5 Mon Sep 17 00:00:00 2001 From: ohemorange Date: Wed, 11 Jul 2018 11:20:36 -0700 Subject: [PATCH 15/19] Update default to ACMEv2 server (#6152) --- certbot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/constants.py b/certbot/constants.py index 46523ce4d..a2de2d27a 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -88,7 +88,7 @@ CLI_DEFAULTS = dict( config_dir="/etc/letsencrypt", work_dir="/var/lib/letsencrypt", logs_dir="/var/log/letsencrypt", - server="https://acme-v01.api.letsencrypt.org/directory", + server="https://acme-v02.api.letsencrypt.org/directory", # Plugins parsers configurator=None, From 7383fc6bf0702aa7fcbac1b54ce3508e0900cd08 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 11 Jul 2018 17:25:48 -0700 Subject: [PATCH 16/19] move values to pytest.ini --- pytest.ini | 2 ++ tools/install_and_test.sh | 3 ++- tox.cover.sh | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..3ecec1912 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --numprocesses auto --pyargs diff --git a/tools/install_and_test.sh b/tools/install_and_test.sh index 819f683aa..e84fbc99e 100755 --- a/tools/install_and_test.sh +++ b/tools/install_and_test.sh @@ -14,6 +14,7 @@ fi temp_cwd=$(mktemp -d) trap "rm -rf $temp_cwd" EXIT +cp pytest.ini "$temp_cwd" set -x for requirement in "$@" ; do @@ -24,6 +25,6 @@ for requirement in "$@" ; do pkg="certbot" fi cd "$temp_cwd" - pytest --numprocesses auto --quiet --pyargs $pkg + pytest --quiet $pkg cd - done diff --git a/tox.cover.sh b/tox.cover.sh index c713327c5..7da2d6b1f 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -61,7 +61,7 @@ cover () { fi pkg_dir=$(echo "$1" | tr _ -) - pytest --cov "$pkg_dir" --cov-append --cov-report= --numprocesses auto --pyargs "$1" + pytest --cov "$pkg_dir" --cov-append --cov-report= "$1" coverage report --fail-under="$min" --include="$pkg_dir/*" --show-missing } From 32676f02c322128263b659d7e34aeb1b12d3ac04 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 11 Jul 2018 17:32:48 -0700 Subject: [PATCH 17/19] warnings are errors --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 3ecec1912..085c7258e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = --numprocesses auto --pyargs +addopts = -Werror --numprocesses auto --pyargs From fdb3c8df4ba181997bb124f7eaf1795ea9f6a1cd Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 11 Jul 2018 17:33:04 -0700 Subject: [PATCH 18/19] s/assertEquals/assertEqual --- acme/acme/client_test.py | 2 +- acme/acme/crypto_util_test.py | 6 +++--- .../certbot_apache/tests/autohsts_test.py | 12 ++++++------ .../certbot_apache/tests/centos_test.py | 8 ++++---- .../certbot_apache/tests/configurator_test.py | 18 +++++++++--------- .../certbot_apache/tests/gentoo_test.py | 6 +++--- .../certbot_apache/tests/parser_test.py | 4 ++-- .../certbot_dns_google/dns_google_test.py | 4 ++-- .../certbot_nginx/tests/configurator_test.py | 10 +++++----- .../certbot_postfix/tests/postconf_test.py | 2 +- certbot/plugins/enhancements_test.py | 6 +++--- certbot/plugins/selection_test.py | 2 +- certbot/tests/cert_manager_test.py | 12 ++++++------ certbot/tests/client_test.py | 4 ++-- certbot/tests/display/ops_test.py | 12 ++++++------ certbot/tests/main_test.py | 2 +- certbot/tests/renewupdater_test.py | 4 ++-- 17 files changed, 57 insertions(+), 57 deletions(-) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index cd31c4ac3..b85aba518 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -659,7 +659,7 @@ class ClientTest(ClientTestBase): def test_revocation_payload(self): obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn) self.assertTrue('reason' in obj.to_partial_json().keys()) - self.assertEquals(self.rsn, obj.to_partial_json()['reason']) + self.assertEqual(self.rsn, obj.to_partial_json()['reason']) def test_revoke_bad_status_raises_error(self): self.response.status_code = http_client.METHOD_NOT_ALLOWED diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 168489d86..44b245bbe 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -209,8 +209,8 @@ class MakeCSRTest(unittest.TestCase): # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): - self.assertEquals(len(csr.get_extensions()), 1) - self.assertEquals(csr.get_extensions()[0].get_data(), + self.assertEqual(len(csr.get_extensions()), 1) + self.assertEqual(csr.get_extensions()[0].get_data(), OpenSSL.crypto.X509Extension( b'subjectAltName', critical=False, @@ -227,7 +227,7 @@ class MakeCSRTest(unittest.TestCase): # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): - self.assertEquals(len(csr.get_extensions()), 2) + self.assertEqual(len(csr.get_extensions()), 2) # NOTE: Ideally we would filter by the TLS Feature OID, but # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID, # and the shortname field is just "UNDEF" diff --git a/certbot-apache/certbot_apache/tests/autohsts_test.py b/certbot-apache/certbot_apache/tests/autohsts_test.py index 73da33f15..aa35dbe30 100644 --- a/certbot-apache/certbot_apache/tests/autohsts_test.py +++ b/certbot-apache/certbot_apache/tests/autohsts_test.py @@ -64,12 +64,12 @@ class AutoHSTSTest(util.ApacheTest): self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"]) # Verify initial value - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), initial_val) # Increase self.config.update_autohsts(mock.MagicMock()) # Verify increased value - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), inc_val) self.assertTrue(mock_prepare.called) @@ -80,7 +80,7 @@ class AutoHSTSTest(util.ApacheTest): initial_val = maxage.format(constants.AUTOHSTS_STEPS[0]) self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"]) # Verify initial value - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), initial_val) self.config.update_autohsts(mock.MagicMock()) @@ -117,11 +117,11 @@ class AutoHSTSTest(util.ApacheTest): self.config.update_autohsts(mock.MagicMock()) # Value should match pre-permanent increment step cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1]) - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), cur_val) # Make permanent self.config.deploy_autohsts(mock_lineage) - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), max_val) def test_autohsts_update_noop(self): @@ -153,7 +153,7 @@ class AutoHSTSTest(util.ApacheTest): mock_id.return_value = "1234567" self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com", "ocspvhost.com"]) - self.assertEquals(mock_id.call_count, 1) + self.assertEqual(mock_id.call_count, 1) def test_autohsts_remove_orphaned(self): # pylint: disable=protected-access diff --git a/certbot-apache/certbot_apache/tests/centos_test.py b/certbot-apache/certbot_apache/tests/centos_test.py index 4ee8b5dcf..bc15fc6c7 100644 --- a/certbot-apache/certbot_apache/tests/centos_test.py +++ b/certbot-apache/certbot_apache/tests/centos_test.py @@ -81,9 +81,9 @@ class MultipleVhostsTestCentOS(util.ApacheTest): mock_osi.return_value = ("centos", "7") self.config.parser.update_runtime_variables() - self.assertEquals(mock_get.call_count, 3) - self.assertEquals(len(self.config.parser.modules), 4) - self.assertEquals(len(self.config.parser.variables), 2) + self.assertEqual(mock_get.call_count, 3) + self.assertEqual(len(self.config.parser.modules), 4) + self.assertEqual(len(self.config.parser.variables), 2) self.assertTrue("TEST2" in self.config.parser.variables.keys()) self.assertTrue("mod_another.c" in self.config.parser.modules) @@ -127,7 +127,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest): def test_alt_restart_works(self, mock_run_script): mock_run_script.side_effect = [None, errors.SubprocessError, None] self.config.restart() - self.assertEquals(mock_run_script.call_count, 3) + self.assertEqual(mock_run_script.call_count, 3) @mock.patch("certbot_apache.configurator.util.run_script") def test_alt_restart_errors(self, mock_run_script): diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 350262634..ed61f5cf8 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1401,11 +1401,11 @@ class MultipleVhostsTest(util.ApacheTest): vhs = self.config._choose_vhosts_wildcard("*.certbot.demo", create_ssl=True) # Check that the dialog was called with one vh: certbot.demo - self.assertEquals(mock_select_vhs.call_args[0][0][0], self.vh_truth[3]) - self.assertEquals(len(mock_select_vhs.call_args_list), 1) + self.assertEqual(mock_select_vhs.call_args[0][0][0], self.vh_truth[3]) + self.assertEqual(len(mock_select_vhs.call_args_list), 1) # And the actual returned values - self.assertEquals(len(vhs), 1) + self.assertEqual(len(vhs), 1) self.assertTrue(vhs[0].name == "certbot.demo") self.assertTrue(vhs[0].ssl) @@ -1420,7 +1420,7 @@ class MultipleVhostsTest(util.ApacheTest): vhs = self.config._choose_vhosts_wildcard("*.certbot.demo", create_ssl=False) self.assertFalse(mock_makessl.called) - self.assertEquals(vhs[0], self.vh_truth[1]) + self.assertEqual(vhs[0], self.vh_truth[1]) @mock.patch("certbot_apache.configurator.ApacheConfigurator._vhosts_for_wildcard") @mock.patch("certbot_apache.configurator.ApacheConfigurator.make_vhost_ssl") @@ -1433,15 +1433,15 @@ class MultipleVhostsTest(util.ApacheTest): mock_select_vhs.return_value = [self.vh_truth[7]] vhs = self.config._choose_vhosts_wildcard("whatever", create_ssl=True) - self.assertEquals(mock_select_vhs.call_args[0][0][0], self.vh_truth[7]) - self.assertEquals(len(mock_select_vhs.call_args_list), 1) + self.assertEqual(mock_select_vhs.call_args[0][0][0], self.vh_truth[7]) + self.assertEqual(len(mock_select_vhs.call_args_list), 1) # Ensure that make_vhost_ssl was not called, vhost.ssl == true self.assertFalse(mock_makessl.called) # And the actual returned values - self.assertEquals(len(vhs), 1) + self.assertEqual(len(vhs), 1) self.assertTrue(vhs[0].ssl) - self.assertEquals(vhs[0], self.vh_truth[7]) + self.assertEqual(vhs[0], self.vh_truth[7]) def test_deploy_cert_wildcard(self): @@ -1454,7 +1454,7 @@ class MultipleVhostsTest(util.ApacheTest): self.config.deploy_cert("*.wildcard.example.org", "/tmp/path", "/tmp/path", "/tmp/path", "/tmp/path") self.assertTrue(mock_dep.called) - self.assertEquals(len(mock_dep.call_args_list), 1) + self.assertEqual(len(mock_dep.call_args_list), 1) self.assertEqual(self.vh_truth[7], mock_dep.call_args_list[0][0][0]) @mock.patch("certbot_apache.display_ops.select_vhost_multiple") diff --git a/certbot-apache/certbot_apache/tests/gentoo_test.py b/certbot-apache/certbot_apache/tests/gentoo_test.py index d32551267..54b4d6532 100644 --- a/certbot-apache/certbot_apache/tests/gentoo_test.py +++ b/certbot-apache/certbot_apache/tests/gentoo_test.py @@ -121,15 +121,15 @@ class MultipleVhostsTestGentoo(util.ApacheTest): mock_osi.return_value = ("gentoo", "123") self.config.parser.update_runtime_variables() - self.assertEquals(mock_get.call_count, 1) - self.assertEquals(len(self.config.parser.modules), 4) + self.assertEqual(mock_get.call_count, 1) + self.assertEqual(len(self.config.parser.modules), 4) self.assertTrue("mod_another.c" in self.config.parser.modules) @mock.patch("certbot_apache.configurator.util.run_script") def test_alt_restart_works(self, mock_run_script): mock_run_script.side_effect = [None, errors.SubprocessError, None] self.config.restart() - self.assertEquals(mock_run_script.call_count, 3) + self.assertEqual(mock_run_script.call_count, 3) if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/parser_test.py b/certbot-apache/certbot_apache/tests/parser_test.py index f95f1b346..88eb55d22 100644 --- a/certbot-apache/certbot_apache/tests/parser_test.py +++ b/certbot-apache/certbot_apache/tests/parser_test.py @@ -84,7 +84,7 @@ class BasicParserTest(util.ParserTest): self.assertEqual(self.parser.aug.get(match), str(i + 1)) def test_empty_arg(self): - self.assertEquals(None, + self.assertEqual(None, self.parser.get_arg("/files/whatever/nonexistent")) def test_add_dir_to_ifmodssl(self): @@ -303,7 +303,7 @@ class BasicParserTest(util.ParserTest): from certbot_apache.parser import get_aug_path self.parser.add_comment(get_aug_path(self.parser.loc["name"]), "123456") comm = self.parser.find_comments("123456") - self.assertEquals(len(comm), 1) + self.assertEqual(len(comm), 1) self.assertTrue(self.parser.loc["name"] in comm[0]) diff --git a/certbot-dns-google/certbot_dns_google/dns_google_test.py b/certbot-dns-google/certbot_dns_google/dns_google_test.py index b6f6e08b6..2b081885b 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google_test.py +++ b/certbot-dns-google/certbot_dns_google/dns_google_test.py @@ -276,9 +276,9 @@ class GoogleClientTest(unittest.TestCase): [{'managedZones': [{'id': self.zone}]}]) # Record name mocked in setUp found = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org") - self.assertEquals(found, ["\"example-txt-contents\""]) + self.assertEqual(found, ["\"example-txt-contents\""]) not_found = client.get_existing_txt_rrset(self.zone, "nonexistent.tld") - self.assertEquals(not_found, None) + self.assertEqual(not_found, None) @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 4d23f3518..3dada2399 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -177,9 +177,9 @@ class NginxConfiguratorTest(util.NginxTest): def test_ipv6only(self): # ipv6_info: (ipv6_active, ipv6only_present) - self.assertEquals((True, False), self.config.ipv6_info("80")) + self.assertEqual((True, False), self.config.ipv6_info("80")) # Port 443 has ipv6only=on because of ipv6ssl.com vhost - self.assertEquals((True, True), self.config.ipv6_info("443")) + self.assertEqual((True, True), self.config.ipv6_info("443")) def test_ipv6only_detection(self): self.config.version = (1, 3, 1) @@ -790,7 +790,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertTrue(vhost in mock_select_vhs.call_args[0][0]) # And the actual returned values - self.assertEquals(len(vhs), 1) + self.assertEqual(len(vhs), 1) self.assertEqual(vhs[0], vhost) def test_choose_vhosts_wildcard_redirect(self): @@ -806,7 +806,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertTrue(vhost in mock_select_vhs.call_args[0][0]) # And the actual returned values - self.assertEquals(len(vhs), 1) + self.assertEqual(len(vhs), 1) self.assertEqual(vhs[0], vhost) def test_deploy_cert_wildcard(self): @@ -821,7 +821,7 @@ class NginxConfiguratorTest(util.NginxTest): self.config.deploy_cert("*.com", "/tmp/path", "/tmp/path", "/tmp/path", "/tmp/path") self.assertTrue(mock_dep.called) - self.assertEquals(len(mock_dep.call_args_list), 1) + self.assertEqual(len(mock_dep.call_args_list), 1) self.assertEqual(vhost, mock_dep.call_args_list[0][0][0]) @mock.patch("certbot_nginx.display_ops.select_vhost_multiple") diff --git a/certbot-postfix/certbot_postfix/tests/postconf_test.py b/certbot-postfix/certbot_postfix/tests/postconf_test.py index 91617d410..01a43773d 100644 --- a/certbot-postfix/certbot_postfix/tests/postconf_test.py +++ b/certbot-postfix/certbot_postfix/tests/postconf_test.py @@ -85,7 +85,7 @@ class PostConfTest(unittest.TestCase): self.config.set('extra_param', 'another_value') self.config.flush() arguments = mock_out.call_args_list[-1][0][0] - self.assertEquals('-e', arguments[0]) + self.assertEqual('-e', arguments[0]) self.assertTrue('default_parameter=new_value' in arguments) self.assertTrue('extra_param=another_value' in arguments) diff --git a/certbot/plugins/enhancements_test.py b/certbot/plugins/enhancements_test.py index b69dc9836..22f6f54e9 100644 --- a/certbot/plugins/enhancements_test.py +++ b/certbot/plugins/enhancements_test.py @@ -37,11 +37,11 @@ class EnhancementTest(test_util.ConfigTestCase): self.assertTrue([i for i in enabled if i["name"] == "somethingelse"]) def test_are_requested(self): - self.assertEquals( + self.assertEqual( len([i for i in enhancements.enabled_enhancements(self.config)]), 0) self.assertFalse(enhancements.are_requested(self.config)) self.config.auto_hsts = True - self.assertEquals( + self.assertEqual( len([i for i in enhancements.enabled_enhancements(self.config)]), 1) self.assertTrue(enhancements.are_requested(self.config)) @@ -57,7 +57,7 @@ class EnhancementTest(test_util.ConfigTestCase): lineage = "lineage" enhancements.enable(lineage, domains, self.mockinstaller, self.config) self.assertTrue(self.mockinstaller.enable_autohsts.called) - self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0], + self.assertEqual(self.mockinstaller.enable_autohsts.call_args[0], (lineage, domains)) diff --git a/certbot/plugins/selection_test.py b/certbot/plugins/selection_test.py index 44d64ab8e..5f8e42516 100644 --- a/certbot/plugins/selection_test.py +++ b/certbot/plugins/selection_test.py @@ -197,7 +197,7 @@ class GetUnpreparedInstallerTest(test_util.ConfigTestCase): def test_no_installer_defined(self): self.config.configurator = None - self.assertEquals(self._call(), None) + self.assertEqual(self._call(), None) def test_no_available_installers(self): self.config.configurator = "apache" diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 6ec1d4f5c..3ef1709f5 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -589,7 +589,7 @@ class GetCertnameTest(unittest.TestCase): from certbot import cert_manager prompt = "Which certificate would you" self.mock_get_utility().menu.return_value = (display_util.OK, 0) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=False), ['example.com']) self.assertTrue( @@ -603,11 +603,11 @@ class GetCertnameTest(unittest.TestCase): from certbot import cert_manager prompt = "custom prompt" self.mock_get_utility().menu.return_value = (display_util.OK, 0) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=False, custom_prompt=prompt), ['example.com']) - self.assertEquals(self.mock_get_utility().menu.call_args[0][0], + self.assertEqual(self.mock_get_utility().menu.call_args[0][0], prompt) @mock.patch('certbot.storage.renewal_conf_files') @@ -631,7 +631,7 @@ class GetCertnameTest(unittest.TestCase): prompt = "Which certificate(s) would you" self.mock_get_utility().checklist.return_value = (display_util.OK, ['example.com']) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=True), ['example.com']) self.assertTrue( @@ -646,11 +646,11 @@ class GetCertnameTest(unittest.TestCase): prompt = "custom prompt" self.mock_get_utility().checklist.return_value = (display_util.OK, ['example.com']) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=True, custom_prompt=prompt), ['example.com']) - self.assertEquals( + self.assertEqual( self.mock_get_utility().checklist.call_args[0][0], prompt) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 70ab4c798..a4e70ce35 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -487,7 +487,7 @@ class EnhanceConfigTest(ClientTestCommon): self.config.hsts = True self._test_with_already_existing() self.assertTrue(mock_log.warning.called) - self.assertEquals(mock_log.warning.call_args[0][1], + self.assertEqual(mock_log.warning.call_args[0][1], 'Strict-Transport-Security') @mock.patch("certbot.client.logger") @@ -495,7 +495,7 @@ class EnhanceConfigTest(ClientTestCommon): self.config.redirect = True self._test_with_already_existing() self.assertTrue(mock_log.warning.called) - self.assertEquals(mock_log.warning.call_args[0][1], + self.assertEqual(mock_log.warning.call_args[0][1], 'redirect') def test_no_ask_hsts(self): diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 9de8c5e9a..9ad0ce87a 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -502,9 +502,9 @@ class ChooseValuesTest(unittest.TestCase): items = ["first", "second", "third"] mock_util().checklist.return_value = (display_util.OK, [items[2]]) result = self._call(items, None) - self.assertEquals(result, [items[2]]) + self.assertEqual(result, [items[2]]) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], None) + self.assertEqual(mock_util().checklist.call_args[0][0], None) @test_util.patch_get_utility("certbot.display.ops.z_util") def test_choose_names_success_question(self, mock_util): @@ -512,9 +512,9 @@ class ChooseValuesTest(unittest.TestCase): question = "Which one?" mock_util().checklist.return_value = (display_util.OK, [items[1]]) result = self._call(items, question) - self.assertEquals(result, [items[1]]) + self.assertEqual(result, [items[1]]) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], question) + self.assertEqual(mock_util().checklist.call_args[0][0], question) @test_util.patch_get_utility("certbot.display.ops.z_util") def test_choose_names_user_cancel(self, mock_util): @@ -522,9 +522,9 @@ class ChooseValuesTest(unittest.TestCase): question = "Want to cancel?" mock_util().checklist.return_value = (display_util.CANCEL, []) result = self._call(items, question) - self.assertEquals(result, []) + self.assertEqual(result, []) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], question) + self.assertEqual(mock_util().checklist.call_args[0][0], question) if __name__ == "__main__": diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index cc4e6c293..b4e0d5581 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1712,7 +1712,7 @@ class EnhanceTest(test_util.ConfigTestCase): mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent") self._call(['enhance', '--auto-hsts']) self.assertTrue(self.mockinstaller.enable_autohsts.called) - self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0][1], + self.assertEqual(self.mockinstaller.enable_autohsts.call_args[0][1], ["example.com", "another.tld"]) @mock.patch('certbot.cert_manager.lineage_for_certname') diff --git a/certbot/tests/renewupdater_test.py b/certbot/tests/renewupdater_test.py index 5a362072c..5fe188c42 100644 --- a/certbot/tests/renewupdater_test.py +++ b/certbot/tests/renewupdater_test.py @@ -53,7 +53,7 @@ class RenewUpdaterTest(test_util.ConfigTestCase): self.config.dry_run = True updater.run_generic_updaters(self.config, None, None) self.assertTrue(mock_log.called) - self.assertEquals(mock_log.call_args[0][0], + self.assertEqual(mock_log.call_args[0][0], "Skipping updaters in dry-run mode.") @mock.patch("certbot.updater.logger.debug") @@ -61,7 +61,7 @@ class RenewUpdaterTest(test_util.ConfigTestCase): self.config.dry_run = True updater.run_renewal_deployer(self.config, None, None) self.assertTrue(mock_log.called) - self.assertEquals(mock_log.call_args[0][0], + self.assertEqual(mock_log.call_args[0][0], "Skipping renewal deployer in dry-run mode.") @mock.patch('certbot.plugins.selection.get_unprepared_installer') From aad266369597281c79022cc4d807bc2be2b08b43 Mon Sep 17 00:00:00 2001 From: Erica Portnoy Date: Tue, 30 Oct 2018 17:13:40 -0700 Subject: [PATCH 19/19] Only error on DeprecationWarnings --- pytest.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 085c7258e..2669c1105 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,4 @@ [pytest] -addopts = -Werror --numprocesses auto --pyargs +addopts = --numprocesses auto --pyargs +filterwarnings = + error::DeprecationWarning