├── .coveragerc ├── .github └── workflows │ ├── lint.yml │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .pylintrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── certbot_dns_hetzner ├── __init__.py ├── dns_hetzner.py ├── dns_hetzner_test.py └── fakes.py ├── codecov.yml ├── setup.cfg ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | 2 | [run] 3 | source = . 4 | omit = .venv/*,*test*,setup.py,*__init__.py -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | on: [push] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@master 8 | - name: Setup Python 9 | uses: actions/setup-python@master 10 | with: 11 | python-version: '3.11' 12 | - name: Dependencies 13 | run: | 14 | python -m pip install --upgrade pip 15 | pip install -e . 16 | - name: Linting 17 | run: | 18 | pip install pylint==2.15.8 19 | python -m pylint --reports=n --rcfile=.pylintrc certbot_dns_hetzner 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish on pypi 2 | on: 3 | push: 4 | tags: 5 | jobs: 6 | publish: 7 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Publish Python Package 13 | uses: mariamrf/py-package-publish-action@v1.0.0 14 | with: 15 | python_version: '3.11' 16 | env: 17 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 18 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests, Coverage 2 | on: [push] 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ] 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Setup Python 12 | uses: actions/setup-python@master 13 | with: 14 | python-version: ${{ matrix.python-version }} 15 | - name: Dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install -e . 19 | - name: Testing with tox 20 | run: | 21 | pip install tox tox-gh-actions 22 | tox 23 | - name: Generate coverage report 24 | run: | 25 | pip install pytest 26 | pip install pytest-cov 27 | pytest --cov=./ --cov-report=xml 28 | - name: Upload coverage to Codecov 29 | uses: codecov/codecov-action@v3 30 | with: 31 | token: ${{ secrets.CODECOV_TOKEN }} 32 | files: ./coverage.xml 33 | flags: unittests 34 | name: codecov-umbrella 35 | fail_ci_if_error: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.egg-info/ 4 | .eggs/ 5 | build/ 6 | dist*/ 7 | /venv*/ 8 | /kgs/ 9 | /.tox/ 10 | /releases*/ 11 | /log* 12 | letsencrypt.log 13 | certbot.log 14 | letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 15 | coverage.xml 16 | 17 | # coverage 18 | .coverage 19 | /htmlcov/ 20 | 21 | /.vagrant 22 | 23 | tags 24 | 25 | # editor temporary files 26 | *~ 27 | *.sw? 28 | \#*# 29 | .idea 30 | .ropeproject 31 | .vscode 32 | 33 | # auth --cert-path --chain-path 34 | /*.pem 35 | 36 | # letstest 37 | tests/letstest/letest-*/ 38 | tests/letstest/*.pem 39 | tests/letstest/venv/ 40 | tests/letstest/venv3/ 41 | 42 | .venv 43 | 44 | # pytest cache 45 | .cache 46 | .mypy_cache/ 47 | .pytest_cache/ 48 | 49 | # docker files 50 | .docker 51 | 52 | # certbot tests 53 | .certbot_test_workspace 54 | **/assets/pebble* 55 | **/assets/challtestsrv* 56 | 57 | # snap files 58 | .snapcraft 59 | parts 60 | prime 61 | stage 62 | *.snap 63 | snap-constraints.txt 64 | qemu-* 65 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | # Regular expression which should only match function or class names that do 3 | # not require a docstring. 4 | no-docstring-rgx=(__.*__)|(test_[A-Za-z0-9_]*)|(_.*)|(.*Test$) 5 | 6 | [FORMAT] 7 | max-line-length=120 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.3 4 | 5 | * fix faulty publish script in github actions 6 | 7 | ## 1.0.2 8 | 9 | * fixes bug #2 10 | 11 | ## 1.0.1 12 | 13 | * package available on pypi 14 | 15 | ## 1.0.0 16 | 17 | * Initial release 18 | * Working certbot plugin to authenticate via dns-01 challenge on dns.hetzner.com 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 Electronic Frontier Foundation and others 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Apache License 16 | Version 2.0, January 2004 17 | http://www.apache.org/licenses/ 18 | 19 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 20 | 21 | 1. Definitions. 22 | 23 | "License" shall mean the terms and conditions for use, reproduction, 24 | and distribution as defined by Sections 1 through 9 of this document. 25 | 26 | "Licensor" shall mean the copyright owner or entity authorized by 27 | the copyright owner that is granting the License. 28 | 29 | "Legal Entity" shall mean the union of the acting entity and all 30 | other entities that control, are controlled by, or are under common 31 | control with that entity. For the purposes of this definition, 32 | "control" means (i) the power, direct or indirect, to cause the 33 | direction or management of such entity, whether by contract or 34 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 35 | outstanding shares, or (iii) beneficial ownership of such entity. 36 | 37 | "You" (or "Your") shall mean an individual or Legal Entity 38 | exercising permissions granted by this License. 39 | 40 | "Source" form shall mean the preferred form for making modifications, 41 | including but not limited to software source code, documentation 42 | source, and configuration files. 43 | 44 | "Object" form shall mean any form resulting from mechanical 45 | transformation or translation of a Source form, including but 46 | not limited to compiled object code, generated documentation, 47 | and conversions to other media types. 48 | 49 | "Work" shall mean the work of authorship, whether in Source or 50 | Object form, made available under the License, as indicated by a 51 | copyright notice that is included in or attached to the work 52 | (an example is provided in the Appendix below). 53 | 54 | "Derivative Works" shall mean any work, whether in Source or Object 55 | form, that is based on (or derived from) the Work and for which the 56 | editorial revisions, annotations, elaborations, or other modifications 57 | represent, as a whole, an original work of authorship. For the purposes 58 | of this License, Derivative Works shall not include works that remain 59 | separable from, or merely link (or bind by name) to the interfaces of, 60 | the Work and Derivative Works thereof. 61 | 62 | "Contribution" shall mean any work of authorship, including 63 | the original version of the Work and any modifications or additions 64 | to that Work or Derivative Works thereof, that is intentionally 65 | submitted to Licensor for inclusion in the Work by the copyright owner 66 | or by an individual or Legal Entity authorized to submit on behalf of 67 | the copyright owner. For the purposes of this definition, "submitted" 68 | means any form of electronic, verbal, or written communication sent 69 | to the Licensor or its representatives, including but not limited to 70 | communication on electronic mailing lists, source code control systems, 71 | and issue tracking systems that are managed by, or on behalf of, the 72 | Licensor for the purpose of discussing and improving the Work, but 73 | excluding communication that is conspicuously marked or otherwise 74 | designated in writing by the copyright owner as "Not a Contribution." 75 | 76 | "Contributor" shall mean Licensor and any individual or Legal Entity 77 | on behalf of whom a Contribution has been received by Licensor and 78 | subsequently incorporated within the Work. 79 | 80 | 2. Grant of Copyright License. Subject to the terms and conditions of 81 | this License, each Contributor hereby grants to You a perpetual, 82 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 83 | copyright license to reproduce, prepare Derivative Works of, 84 | publicly display, publicly perform, sublicense, and distribute the 85 | Work and such Derivative Works in Source or Object form. 86 | 87 | 3. Grant of Patent License. Subject to the terms and conditions of 88 | this License, each Contributor hereby grants to You a perpetual, 89 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 90 | (except as stated in this section) patent license to make, have made, 91 | use, offer to sell, sell, import, and otherwise transfer the Work, 92 | where such license applies only to those patent claims licensable 93 | by such Contributor that are necessarily infringed by their 94 | Contribution(s) alone or by combination of their Contribution(s) 95 | with the Work to which such Contribution(s) was submitted. If You 96 | institute patent litigation against any entity (including a 97 | cross-claim or counterclaim in a lawsuit) alleging that the Work 98 | or a Contribution incorporated within the Work constitutes direct 99 | or contributory patent infringement, then any patent licenses 100 | granted to You under this License for that Work shall terminate 101 | as of the date such litigation is filed. 102 | 103 | 4. Redistribution. You may reproduce and distribute copies of the 104 | Work or Derivative Works thereof in any medium, with or without 105 | modifications, and in Source or Object form, provided that You 106 | meet the following conditions: 107 | 108 | (a) You must give any other recipients of the Work or 109 | Derivative Works a copy of this License; and 110 | 111 | (b) You must cause any modified files to carry prominent notices 112 | stating that You changed the files; and 113 | 114 | (c) You must retain, in the Source form of any Derivative Works 115 | that You distribute, all copyright, patent, trademark, and 116 | attribution notices from the Source form of the Work, 117 | excluding those notices that do not pertain to any part of 118 | the Derivative Works; and 119 | 120 | (d) If the Work includes a "NOTICE" text file as part of its 121 | distribution, then any Derivative Works that You distribute must 122 | include a readable copy of the attribution notices contained 123 | within such NOTICE file, excluding those notices that do not 124 | pertain to any part of the Derivative Works, in at least one 125 | of the following places: within a NOTICE text file distributed 126 | as part of the Derivative Works; within the Source form or 127 | documentation, if provided along with the Derivative Works; or, 128 | within a display generated by the Derivative Works, if and 129 | wherever such third-party notices normally appear. The contents 130 | of the NOTICE file are for informational purposes only and 131 | do not modify the License. You may add Your own attribution 132 | notices within Derivative Works that You distribute, alongside 133 | or as an addendum to the NOTICE text from the Work, provided 134 | that such additional attribution notices cannot be construed 135 | as modifying the License. 136 | 137 | You may add Your own copyright statement to Your modifications and 138 | may provide additional or different license terms and conditions 139 | for use, reproduction, or distribution of Your modifications, or 140 | for any such Derivative Works as a whole, provided Your use, 141 | reproduction, and distribution of the Work otherwise complies with 142 | the conditions stated in this License. 143 | 144 | 5. Submission of Contributions. Unless You explicitly state otherwise, 145 | any Contribution intentionally submitted for inclusion in the Work 146 | by You to the Licensor shall be under the terms and conditions of 147 | this License, without any additional terms or conditions. 148 | Notwithstanding the above, nothing herein shall supersede or modify 149 | the terms of any separate license agreement you may have executed 150 | with Licensor regarding such Contributions. 151 | 152 | 6. Trademarks. This License does not grant permission to use the trade 153 | names, trademarks, service marks, or product names of the Licensor, 154 | except as required for reasonable and customary use in describing the 155 | origin of the Work and reproducing the content of the NOTICE file. 156 | 157 | 7. Disclaimer of Warranty. Unless required by applicable law or 158 | agreed to in writing, Licensor provides the Work (and each 159 | Contributor provides its Contributions) on an "AS IS" BASIS, 160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 161 | implied, including, without limitation, any warranties or conditions 162 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 163 | PARTICULAR PURPOSE. You are solely responsible for determining the 164 | appropriateness of using or redistributing the Work and assume any 165 | risks associated with Your exercise of permissions under this License. 166 | 167 | 8. Limitation of Liability. In no event and under no legal theory, 168 | whether in tort (including negligence), contract, or otherwise, 169 | unless required by applicable law (such as deliberate and grossly 170 | negligent acts) or agreed to in writing, shall any Contributor be 171 | liable to You for damages, including any direct, indirect, special, 172 | incidental, or consequential damages of any character arising as a 173 | result of this License or out of the use or inability to use the 174 | Work (including but not limited to damages for loss of goodwill, 175 | work stoppage, computer failure or malfunction, or any and all 176 | other commercial damages or losses), even if such Contributor 177 | has been advised of the possibility of such damages. 178 | 179 | 9. Accepting Warranty or Additional Liability. While redistributing 180 | the Work or Derivative Works thereof, You may choose to offer, 181 | and charge a fee for, acceptance of support, warranty, indemnity, 182 | or other liability obligations and/or rights consistent with this 183 | License. However, in accepting such obligations, You may act only 184 | on Your own behalf and on Your sole responsibility, not on behalf 185 | of any other Contributor, and only if You agree to indemnify, 186 | defend, and hold each Contributor harmless for any liability 187 | incurred by, or claims asserted against, such Contributor by reason 188 | of your accepting any such warranty or additional liability. 189 | 190 | END OF TERMS AND CONDITIONS 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hetzner DNS Authenticator certbot plugin 2 | [![codecov](https://codecov.io/gh/ctrlaltcoop/certbot-dns-hetzner/branch/main/graph/badge.svg?token=3XJVTPZ0AM)](https://codecov.io/gh/ctrlaltcoop/certbot-dns-hetzner) 3 | ![Tests, Coverage](https://github.com/ctrlaltcoop/certbot-dns-hetzner/workflows/Tests,%20Coverage/badge.svg?branch=main) 4 | [![PyPI version](https://badge.fury.io/py/certbot-dns-hetzner.svg)](https://badge.fury.io/py/certbot-dns-hetzner) 5 | ![Supported Python](https://img.shields.io/pypi/pyversions/certbot-dns-hetzner) 6 | 7 | This certbot plugin automates the process of 8 | completing a dns-01 challenge by creating, and 9 | subsequently removing, TXT records using the Hetzner DNS API. 10 | 11 | ## Requirements 12 | 13 | ### For certbot < 2 14 | 15 | Notice that this plugin is only supporting certbot>=2.0 from 2.0 onwards. For older certbot versions use 1.x releases. 16 | 17 | ## Install 18 | 19 | Install this package via pip in the same python environment where you installed your certbot. 20 | 21 | ``` 22 | pip install certbot-dns-hetzner 23 | ``` 24 | 25 | ## Usage 26 | 27 | To start using DNS authentication for the Hetzner DNS API, pass the following arguments on certbot's command line: 28 | 29 | | Option | Description | 30 | |------------------------------------------------------------|--------------------------------------------------| 31 | | `--authenticator dns-hetzner` | select the authenticator plugin (Required) | 32 | | `--dns-hetzner-credentials` | Hetzner DNS API credentials INI file. (Required) | 33 | | `--dns-hetzner-propagation-seconds` | Seconds to wait for the TXT record to propagate | 34 | 35 | ## Credentials 36 | 37 | 38 | From the hetzner DNS control panel at https://dns.hetzner.com go to "API Tokens" and add a personal access token. 39 | Please make sure to use the absolute path - some users experienced problems with relative paths. 40 | 41 | An example ``credentials.ini`` file: 42 | 43 | ```ini 44 | dns_hetzner_api_token = nohnah4zoo9Kiejee9aGh0thoopee2sa 45 | ``` 46 | ## Examples 47 | To acquire a certificate for `example.com` 48 | ```shell script 49 | certbot certonly \\ 50 | --authenticator dns-hetzner \\ 51 | --dns-hetzner-credentials /path/to/my/hetzner.ini \\ 52 | -d example.com 53 | ``` 54 | 55 | To acquire a certificate for ``*.example.com`` 56 | ```shell script 57 | certbot certonly \\ 58 | --authenticator dns-hetzner \\ 59 | --dns-hetzner-credentials /path/to/my/hetzner.ini \\ 60 | -d '*.example.com' 61 | ``` 62 | 63 | ## Troubleshooting 64 | 65 | ### Plugin not showing up 66 | If `certbot plugins` does not show the installed plugin, you might need to set `CERTBOT_PLUGIN_PATH`. 67 | ``` 68 | CERTBOT_PLUGIN_PATH=/usr/local/lib/python3.9/site-packages/ certbot renew 69 | ``` 70 | [See letsencrypt community thread](https://community.letsencrypt.org/t/how-do-i-make-certbot-find-use-an-installed-plugin/198647/5) 71 | 72 | ### Renewing certificate fails 73 | Please ensure to use an absolute path for the credentials file - some users experienced problems with relative paths. 74 | 75 | ### Not working with snap 76 | We did not nor plan to support snap - it was created from this [repo](https://github.com/BigMichi1/certbot-dns-hetzner). 77 | Feel free to start a new snap package yourself - we would happily link it here. 78 | 79 | ## Thanks to 80 | 81 | Of course certbot, which examples and documentation I used to implement this plugin. And to https://github.com/m42e/certbot-dns-ispconfig which served as an excellent example and README template as well. 82 | 83 | -------------------------------------------------------------------------------- /certbot_dns_hetzner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlaltcoop/certbot-dns-hetzner/3403ec753793bde26020b91d3b77aa064931954b/certbot_dns_hetzner/__init__.py -------------------------------------------------------------------------------- /certbot_dns_hetzner/dns_hetzner.py: -------------------------------------------------------------------------------- 1 | """DNS Authenticator for Hetzner DNS.""" 2 | import tldextract 3 | from certbot.plugins import dns_common 4 | from lexicon.client import Client 5 | from lexicon.config import ConfigResolver 6 | 7 | TTL = 60 8 | 9 | 10 | class Authenticator(dns_common.DNSAuthenticator): 11 | """DNS Authenticator for Hetzner 12 | This Authenticator uses the Hetzner DNS API to fulfill a dns-01 challenge. 13 | """ 14 | 15 | description = ( 16 | "Obtain certificates using a DNS TXT record (if you are using Hetzner for DNS)." 17 | ) 18 | 19 | def __init__(self, *args, **kwargs): 20 | super().__init__(*args, **kwargs) 21 | self.credentials = None 22 | 23 | @classmethod 24 | def add_parser_arguments(cls, add, default_propagation_seconds = 60): 25 | super(Authenticator, cls).add_parser_arguments( 26 | add, default_propagation_seconds=default_propagation_seconds 27 | ) 28 | add("credentials", help="Hetzner credentials INI file.") 29 | 30 | def more_info(self): # pylint: disable=missing-function-docstring 31 | return ( 32 | "This plugin configures a DNS TXT record to respond to a dns-01 challenge using " 33 | + "the Hetzner API." 34 | ) 35 | 36 | def _setup_credentials(self): 37 | self.credentials = self._configure_credentials( 38 | "credentials", 39 | "Hetzner credentials INI file", 40 | { 41 | "api_token": "Hetzner API Token from 'https://dns.hetzner.com/settings/api-token'", 42 | }, 43 | ) 44 | 45 | @staticmethod 46 | def _get_zone(domain): 47 | extract = tldextract.TLDExtract() 48 | zone_name = extract(domain, include_psl_private_domains=True) 49 | return '.'.join([zone_name.domain, zone_name.suffix]) 50 | 51 | def _perform(self, domain, validation_name, validation): 52 | with self._get_hetzner_client(domain) as client: 53 | client.create_record("TXT", self._fqdn_format(validation_name), validation) 54 | 55 | def _cleanup(self, domain, validation_name, validation): 56 | with self._get_hetzner_client(domain) as client: 57 | client.delete_record(None, "TXT", self._fqdn_format(validation_name), validation) 58 | 59 | def _get_hetzner_client(self, domain): 60 | config = ConfigResolver().with_env().with_dict({ 61 | "provider_name": "hetzner", 62 | "hetzner": { 63 | "auth_token": self.credentials.conf("api_token") 64 | }, 65 | 66 | "ttl": TTL, 67 | "domain": self._get_zone(domain), 68 | }) 69 | return Client(config) 70 | 71 | @staticmethod 72 | def _fqdn_format(name): 73 | if not name.endswith("."): 74 | return f"{name}." 75 | return name 76 | -------------------------------------------------------------------------------- /certbot_dns_hetzner/dns_hetzner_test.py: -------------------------------------------------------------------------------- 1 | """Tests for certbot_dns_hetzner.dns_hetzner.""" 2 | 3 | import unittest 4 | 5 | from unittest import mock 6 | 7 | from certbot.compat import os 8 | from certbot.errors import PluginError 9 | from certbot.plugins import dns_test_common 10 | from certbot.plugins.dns_test_common import DOMAIN 11 | from certbot.tests import util as test_util 12 | 13 | from certbot_dns_hetzner.fakes import FAKE_API_TOKEN, FAKE_RECORD 14 | 15 | 16 | patch_display_util = test_util.patch_display_util 17 | 18 | 19 | class AuthenticatorTest( 20 | test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest 21 | ): 22 | """ 23 | Test for Hetzner DNS Authenticator 24 | """ 25 | 26 | def setUp(self): 27 | super().setUp() 28 | from certbot_dns_hetzner.dns_hetzner import Authenticator # pylint: disable=import-outside-toplevel 29 | 30 | path = os.path.join(self.tempdir, "fake_credentials.ini") 31 | dns_test_common.write( 32 | { 33 | "hetzner_api_token": FAKE_API_TOKEN, 34 | }, 35 | path, 36 | ) 37 | 38 | super().setUp() 39 | self.config = mock.MagicMock( 40 | hetzner_credentials=path, hetzner_propagation_seconds=0 41 | ) # don't wait during tests 42 | 43 | self.auth = Authenticator(self.config, "hetzner") 44 | 45 | self.mock_client = mock.MagicMock() 46 | 47 | mock_client_wrapper = mock.MagicMock() 48 | mock_client_wrapper.__enter__ = mock.MagicMock( 49 | return_value=self.mock_client 50 | ) 51 | 52 | # _get_ispconfig_client | pylint: disable=protected-access 53 | self.auth._get_hetzner_client = mock.MagicMock( 54 | return_value=mock_client_wrapper 55 | ) 56 | 57 | @patch_display_util() 58 | def test_perform(self, _unused_mock_get_utility): 59 | self.mock_client.create_record.return_value = FAKE_RECORD 60 | self.auth.perform([self.achall]) 61 | self.mock_client.create_record.assert_called_with( 62 | "TXT", "_acme-challenge." + DOMAIN + ".", mock.ANY 63 | ) 64 | 65 | def test_perform_but_raises_plugin_error(self): 66 | self.mock_client.create_record.side_effect = mock.MagicMock( 67 | side_effect=PluginError() 68 | ) 69 | self.assertRaises(PluginError, self.auth.perform, [self.achall]) 70 | self.mock_client.create_record.assert_called_with( 71 | "TXT", "_acme-challenge." + DOMAIN + ".", mock.ANY 72 | ) 73 | 74 | @patch_display_util() 75 | def test_cleanup(self, _unused_mock_get_utility): 76 | self.mock_client.create_record.return_value = FAKE_RECORD 77 | # _attempt_cleanup | pylint: disable=protected-access 78 | self.auth.perform([self.achall]) 79 | self.auth._attempt_cleanup = True 80 | self.auth.cleanup([self.achall]) 81 | 82 | self.mock_client.delete_record.assert_called_with( 83 | None, "TXT", "_acme-challenge." + DOMAIN + ".", mock.ANY 84 | ) 85 | 86 | @patch_display_util() 87 | def test_cleanup_but_raises_plugin_error(self, _unused_mock_get_utility): 88 | self.mock_client.create_record.return_value = FAKE_RECORD 89 | self.mock_client.delete_record.side_effect = mock.MagicMock( 90 | side_effect=PluginError() 91 | ) 92 | # _attempt_cleanup | pylint: disable=protected-access 93 | self.auth.perform([self.achall]) 94 | self.auth._attempt_cleanup = True 95 | 96 | self.assertRaises(PluginError, self.auth.cleanup, [self.achall]) 97 | self.mock_client.delete_record.assert_called_with( 98 | None, "TXT", "_acme-challenge." + DOMAIN + ".", mock.ANY 99 | ) 100 | 101 | 102 | if __name__ == "__main__": 103 | unittest.main() # pragma: no cover 104 | -------------------------------------------------------------------------------- /certbot_dns_hetzner/fakes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fakes needed for tests 3 | """ 4 | 5 | FAKE_API_TOKEN = "XXXXXXXXXXXXXXXXXXXxxx" 6 | FAKE_RECORD = { 7 | "record": { 8 | "id": "123Fake", 9 | } 10 | } 11 | 12 | FAKE_DOMAIN = "some.domain" 13 | FAKE_ZONE_ID = "xyz" 14 | FAKE_RECORD_ID = "zzz" 15 | FAKE_RECORD_NAME = "thisisarecordname" 16 | 17 | FAKE_RECORD_RESPONSE = { 18 | "record": { 19 | "id": FAKE_RECORD_ID, 20 | "name": "string", 21 | } 22 | } 23 | 24 | FAKE_RECORDS_RESPONSE_WITH_RECORD = { 25 | "records": [ 26 | { 27 | "id": FAKE_RECORD_ID, 28 | "name": FAKE_RECORD_NAME, 29 | } 30 | ] 31 | } 32 | 33 | FAKE_RECORDS_RESPONSE_WITHOUT_RECORD = { 34 | "records": [ 35 | { 36 | "id": "nottheoneuwant", 37 | "name": "string", 38 | } 39 | ] 40 | } 41 | 42 | FAKE_ZONES_RESPONSE_WITH_DOMAIN = { 43 | "zones": [ 44 | { 45 | "id": FAKE_ZONE_ID, 46 | "name": FAKE_DOMAIN, 47 | } 48 | ] 49 | } 50 | 51 | FAKE_ZONES_RESPONSE_WITHOUT_DOMAIN = { 52 | "zones": [ 53 | { 54 | "id": "string", 55 | "name": "string", 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 80% 6 | threshold: 10% -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | [metadata] 4 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | version = '2.0.1' 7 | 8 | # This package relies on PyOpenSSL, requests, and six, however, it isn't 9 | # specified here to avoid masking the more specific request requirements in 10 | # acme. See https://github.com/pypa/pip/issues/988 for more info. 11 | install_requires = [ 12 | 'certbot>=2.0.0', 13 | 'setuptools', 14 | 'requests', 15 | 'requests-mock', 16 | 'dns-lexicon>=3.11.6', 17 | 'parsedatetime<=2.5;python_version<"3.0"' 18 | ] 19 | 20 | BASE_PATH = os.path.abspath(os.path.dirname(__file__)) 21 | with open(os.path.join(BASE_PATH, "README.md")) as f: 22 | long_description = f.read() 23 | 24 | setup( 25 | name='certbot-dns-hetzner', 26 | version=version, 27 | description="Hetzner DNS Authenticator plugin for Certbot", 28 | long_description=long_description, 29 | long_description_content_type="text/markdown", 30 | url='https://github.com/ctrlaltcoop/certbot-dns-hetzner', 31 | author="ctrl.alt.coop", 32 | author_email='kontakt@ctrl.alt.coop', 33 | license='Apache License 2.0', 34 | python_requires='>=3.7', 35 | classifiers=[ 36 | 'Development Status :: 5 - Production/Stable', 37 | 'Environment :: Plugins', 38 | 'Intended Audience :: System Administrators', 39 | 'License :: OSI Approved :: Apache Software License', 40 | 'Operating System :: POSIX :: Linux', 41 | 'Programming Language :: Python', 42 | 'Programming Language :: Python :: 3', 43 | 'Programming Language :: Python :: 3.7', 44 | 'Programming Language :: Python :: 3.8', 45 | 'Programming Language :: Python :: 3.9', 46 | 'Programming Language :: Python :: 3.10', 47 | 'Programming Language :: Python :: 3.11', 48 | 'Topic :: Internet :: WWW/HTTP', 49 | 'Topic :: Security', 50 | 'Topic :: System :: Installation/Setup', 51 | 'Topic :: System :: Networking', 52 | 'Topic :: System :: Systems Administration', 53 | 'Topic :: Utilities', 54 | ], 55 | 56 | packages=find_packages(), 57 | include_package_data=True, 58 | install_requires=install_requires, 59 | entry_points={ 60 | 'certbot.plugins': [ 61 | 'dns-hetzner = certbot_dns_hetzner.dns_hetzner:Authenticator', 62 | ], 63 | }, 64 | test_suite="certbot_dns_hetzner", 65 | ) 66 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py37,py38,py39,py310,py311 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38, mypy 9 | 3.9: py39, mypy 10 | 3.10: py310, mypy 11 | 3.11: py311, mypy 12 | 13 | [testenv] 14 | deps = pytest 15 | commands = 16 | pytest 17 | --------------------------------------------------------------------------------