certbot-dns-netcup 2.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: certbot-dns-netcup
3
+ Version: 2.0.2
4
+ Summary: netcup DNS Authenticator plugin for Certbot
5
+ Author-email: Thomas Gläßle <thomas@coldfix.de>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Source Code, https://github.com/coldfix/certbot-dns-netcup
8
+ Project-URL: Issue Tracker, https://github.com/coldfix/certbot-dns-netcup/issues
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Plugins
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Internet :: WWW/HTTP
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: System :: Installation/Setup
17
+ Classifier: Topic :: System :: Networking
18
+ Classifier: Topic :: System :: Systems Administration
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.7
21
+ Description-Content-Type: text/x-rst
22
+ License-File: LICENSE.txt
23
+ Requires-Dist: certbot>=2.7
24
+ Requires-Dist: requests
25
+ Dynamic: license-file
26
+
27
+ certbot-dns-netcup
28
+ ==================
29
+
30
+ |Version| |License| |ImageSize|
31
+
32
+ netcup_ DNS Authenticator plugin for certbot_.
33
+
34
+ This plugin automates the process of completing a ``dns-01`` challenge by
35
+ creating, and subsequently removing, ``TXT`` records using the netcup `CCP
36
+ API`_.
37
+
38
+ **Note:** This plugin requires certbot ≥ v2.7.
39
+
40
+ If you need support for older certbot versions, check version 1.4.X and below.
41
+
42
+ .. _netcup: https://www.netcup.de/
43
+ .. _certbot: https://certbot.eff.org/
44
+ .. _CCP API: https://www.netcup.com/en/helpcenter/documentation/domain/our-api
45
+ .. _certbot-dns-cloudflare: https://certbot-dns-cloudflare.readthedocs.io/en/latest/
46
+
47
+
48
+ Installation
49
+ ------------
50
+
51
+ Since this package acts as a plugin for certbot_, the installation method can
52
+ vary depending on how certbot is installed.
53
+
54
+ pip
55
+ ~~~
56
+
57
+ If certbot is installed normally as a python package, the plugin can be
58
+ installed using::
59
+
60
+ pip install certbot-dns-netcup
61
+
62
+ snap
63
+ ~~~~
64
+
65
+ If certbot is installed as a snap, you'll have to install this plugin as
66
+ follows::
67
+
68
+ sudo snap install certbot-dns-netcup
69
+
70
+ Furthermore, the following seems to be required in order to connect the plugin
71
+ to certbot::
72
+
73
+ sudo snap set certbot trust-plugin-with-root=ok
74
+ sudo snap connect certbot:plugin certbot-dns-netcup
75
+
76
+ docker
77
+ ~~~~~~
78
+
79
+ The official docker image of this repository is available from
80
+ `docker.io/coldfix/certbot-dns-netcup`_ or
81
+ `ghcr.io/coldfix/certbot-dns-netcup`_.
82
+
83
+ Accordingly, you can pull the image as follows::
84
+
85
+ docker pull docker.io/coldfix/certbot-dns-netcup
86
+
87
+ or::
88
+
89
+ docker pull ghcr.io/coldfix/certbot-dns-netcup
90
+
91
+ .. _docker.io/coldfix/certbot-dns-netcup: https://hub.docker.com/repository/docker/coldfix/certbot-dns-netcup-testing/general
92
+ .. _ghcr.io/coldfix/certbot-dns-netcup: https://github.com/coldfix/certbot-dns-netcup/pkgs/container/certbot-dns-netcup
93
+
94
+ Usage
95
+ -----
96
+
97
+ To acquire a single certificate for both ``example.com`` and
98
+ ``*.example.com``, waiting 1200 seconds (20min) for DNS propagation::
99
+
100
+ certbot certonly \\
101
+ --authenticator dns-netcup \\
102
+ --dns-netcup-credentials ~/.secrets/certbot/netcup.ini \\
103
+ --dns-netcup-propagation-seconds 1200 \\
104
+ --keep-until-expiring --non-interactive --expand \
105
+ --server https://acme-v02.api.letsencrypt.org/directory \
106
+ -d 'example.com' \\
107
+ -d '*.example.com'
108
+
109
+ The parameters and the format of the credentials file are described below.
110
+
111
+
112
+ Named Arguments
113
+ ---------------
114
+
115
+ To start using DNS authentication for netcup, pass the following arguments on
116
+ certbot's command line:
117
+
118
+ ======================================== =======================
119
+ ``--authenticator dns-netcup`` select the authenticator
120
+ plugin (Required)
121
+
122
+ ``--dns-netcup-credentials FILE`` netcup credentials_
123
+ INI file. (Required)
124
+
125
+ ``--dns-netcup-propagation-seconds NUM`` | waiting time for DNS to propagate before asking
126
+ | the ACME server to verify the DNS record.
127
+ | (Default: 900, Recommended: >= 600)
128
+
129
+ ``--dns-netcup-login-retries NUM`` | Number of login retry attempts in case the
130
+ | netcup API client session times out.
131
+ | (Default: 3, Recommended: >= 1)
132
+
133
+ ``--dns-netcup-zone-name DOMAIN`` | Zone name to operate on (often second-level-domain).
134
+ | Will be determined using brute-force
135
+ | requests if not specified.
136
+ ======================================== =======================
137
+
138
+ **NOTE:**
139
+ You may need to set an unexpectedly high propagation time (≥ 900 seconds) to
140
+ give the netcup DNS time to propagate the entries! This may be annoying when
141
+ calling certbot manually but should not be a problem in automated setups.
142
+ In exceptional cases, 20 minutes may be required. See `#28`_.
143
+
144
+ .. _#28: https://github.com/coldfix/certbot-dns-netcup/issues/28
145
+
146
+
147
+ Credentials
148
+ -----------
149
+
150
+ Use of this plugin requires a configuration file containing netcup API
151
+ credentials, obtained from your netcup `account page`_. See also the `CCP
152
+ API`_ documentation.
153
+
154
+ .. _account page: https://ccp.netcup.net/run/daten_aendern.php?sprung=api
155
+
156
+ An example ``credentials.ini`` file:
157
+
158
+ .. code-block:: ini
159
+
160
+ dns_netcup_customer_id = 123456
161
+ dns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567
162
+ dns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123
163
+
164
+ The path to this file can be provided interactively or using the
165
+ ``--dns-netcup-credentials`` command-line argument. Certbot
166
+ records the path to this file for use during renewal, but does not store the
167
+ file's contents.
168
+
169
+ **CAUTION:** You should protect these API credentials as you would the
170
+ password to your netcup account. Users who can read this file can use these
171
+ credentials to issue arbitrary API calls on your behalf. Users who can cause
172
+ Certbot to run using these credentials can complete a ``dns-01`` challenge to
173
+ acquire new certificates or revoke existing certificates for associated
174
+ domains, even if those domains aren't being managed by this server.
175
+
176
+ Certbot will emit a warning if it detects that the credentials file can be
177
+ accessed by other users on your system. The warning reads "Unsafe permissions
178
+ on credentials configuration file", followed by the path to the credentials
179
+ file. This warning will be emitted each time Certbot uses the credentials file,
180
+ including for renewal, and cannot be silenced except by addressing the issue
181
+ (e.g., by using a command like ``chmod 600`` to restrict access to the file).
182
+
183
+
184
+ Docker
185
+ ------
186
+
187
+ You can pull the latest version of the docker image directly from Docker Hub
188
+ as follows::
189
+
190
+ docker pull coldfix/certbot-dns-netcup
191
+
192
+ Alternatively, the docker image can be built from a local checkout and the
193
+ included ``Dockerfile`` as follows::
194
+
195
+ docker build -t coldfix/certbot-dns-netcup .
196
+
197
+ Once that's finished, the application can be run as follows::
198
+
199
+ docker run --rm \
200
+ -v /var/lib/letsencrypt:/var/lib/letsencrypt \
201
+ -v /etc/letsencrypt:/etc/letsencrypt \
202
+ --cap-drop=all \
203
+ coldfix/certbot-dns-netcup certbot certonly \
204
+ --authenticator dns-netcup \
205
+ --dns-netcup-propagation-seconds 900 \
206
+ --dns-netcup-credentials /var/lib/letsencrypt/netcup_credentials.ini \
207
+ --keep-until-expiring --non-interactive --expand \
208
+ --server https://acme-v02.api.letsencrypt.org/directory \
209
+ --agree-tos --email "webmaster@example.com" \
210
+ -d example.com -d '*.example.com'
211
+
212
+ You may want to change the volumes ``/var/lib/letsencrypt`` and
213
+ ``/etc/letsencrypt`` to local directories where the certificates and
214
+ configuration should be stored.
215
+
216
+
217
+ .. Badges:
218
+
219
+ .. |Version| image:: https://img.shields.io/pypi/v/certbot-dns-netcup.svg
220
+ :target: https://pypi.python.org/pypi/certbot-dns-netcup
221
+ :alt: Version
222
+
223
+ .. |License| image:: https://img.shields.io/pypi/l/certbot-dns-netcup.svg
224
+ :target: https://github.com/coldfix/certbot-dns-netcup/blob/master/LICENSE.txt
225
+ :alt: License: Apache
226
+
227
+ .. |ImageSize| image:: https://img.shields.io/docker/image-size/coldfix/certbot-dns-netcup
228
+ :target: https://hub.docker.com/repository/docker/coldfix/certbot-dns-netcup
229
+ :alt: Docker image size
@@ -0,0 +1,7 @@
1
+ certbot_dns_netcup.py,sha256=Oi2Mijy-zin_otguy6AmL4pLk0s4qqir3Ox-E_amdjc,9819
2
+ certbot_dns_netcup-2.0.2.dist-info/licenses/LICENSE.txt,sha256=LMXecVrlqXbwhvukkwiAyeQhkUFz_IURG_9RTUdXEj8,10786
3
+ certbot_dns_netcup-2.0.2.dist-info/METADATA,sha256=TMPKTDN8BM1X_XDRIR9EY6tQBnxwDm6VK-J-Db323gU,8416
4
+ certbot_dns_netcup-2.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ certbot_dns_netcup-2.0.2.dist-info/entry_points.txt,sha256=IehCRjN03ty7slp0M2AZZds-lreWMooFiKADMRe6Cpw,64
6
+ certbot_dns_netcup-2.0.2.dist-info/top_level.txt,sha256=qRnyOkExBSkekrVie6ME3KVbQgT_gLYJxYj9Vl2xegg,19
7
+ certbot_dns_netcup-2.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [certbot.plugins]
2
+ dns-netcup = certbot_dns_netcup:Authenticator
@@ -0,0 +1,190 @@
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
@@ -0,0 +1 @@
1
+ certbot_dns_netcup
certbot_dns_netcup.py ADDED
@@ -0,0 +1,278 @@
1
+ """
2
+ This module defines a certbot plugin to automate the process of completing a
3
+ ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently
4
+ removing, TXT records using the netcup CCP API.
5
+ """
6
+
7
+ # Keep metadata before any imports (for setup.py)!
8
+ __version__ = '2.0.2'
9
+ __url__ = 'https://github.com/coldfix/certbot-dns-netcup'
10
+ __all__ = ['Authenticator']
11
+
12
+ import json
13
+ import logging
14
+ import requests
15
+
16
+ from certbot.plugins import dns_common
17
+ from certbot.errors import PluginError
18
+
19
+
20
+ API_URL = 'https://www.netcup-wiki.de/wiki/CCP_API'
21
+ API_ENDPOINT = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
22
+
23
+ LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ class Authenticator(dns_common.DNSAuthenticator):
27
+ """DNS Authenticator for netcup
28
+
29
+ This Authenticator uses the netcup API to fulfill a dns-01 challenge.
30
+ """
31
+
32
+ description = ('Obtain certificates using a DNS TXT record (if you are '
33
+ 'using netcup for DNS).')
34
+
35
+ def __init__(self, *args, **kwargs):
36
+ super(Authenticator, self).__init__(*args, **kwargs)
37
+ self.credentials = None
38
+ self.api_session_id = None
39
+
40
+ # DNSAuthenticator overrides
41
+
42
+ @classmethod
43
+ def add_parser_arguments(cls, add):
44
+ super().add_parser_arguments(add, default_propagation_seconds=900)
45
+ add('credentials', help='netcup credentials INI file.')
46
+ add('login-retries', default=3, type=int,
47
+ help="login retry attempts in case of session timeout")
48
+ add('zone-name', help=(
49
+ "zone name to operate on (often second-level-domain). "
50
+ "Will be determined using brute-force requests if not specified."))
51
+
52
+ def more_info(self):
53
+ return ('This plugin configures a DNS TXT record to respond to a '
54
+ 'dns-01 challenge using the netcup API.')
55
+
56
+ def _setup_credentials(self):
57
+ self.credentials = self._configure_credentials(
58
+ 'credentials',
59
+ 'netcup credentials INI file',
60
+ {
61
+ 'customer-id': 'Customer ID associated with netcup account',
62
+ 'api-key': 'Key for CCP API, see {}'.format(API_URL),
63
+ 'api-password': 'Password for CCP API, see {}'.format(API_URL),
64
+ }
65
+ )
66
+
67
+ def _perform(self, domain, validation_name, validation):
68
+ LOGGER.debug("add_dns_record (%s, %s)", domain, validation_name)
69
+ client = self._get_netcup_client()
70
+ self._determine_zone(domain, lambda domain: (
71
+ client.add_dns_record(domain, validation_name, validation)))
72
+
73
+ def _cleanup(self, domain, validation_name, validation):
74
+ LOGGER.debug("delete_dns_record(%s, %s)", domain, validation_name)
75
+ client = self._get_netcup_client()
76
+ self._determine_zone(domain, lambda domain: (
77
+ client.delete_dns_record(domain, validation_name, validation)))
78
+
79
+ def _determine_zone(self, domain, func):
80
+ """Find the domain_id for a given domain."""
81
+ zone_name = self.conf('zone-name')
82
+ if zone_name:
83
+ return func(zone_name)
84
+
85
+ domain_name_guesses = dns_common.base_domain_name_guesses(domain)
86
+
87
+ for domain_name in domain_name_guesses:
88
+ try:
89
+ return func(domain_name)
90
+ except NetcupZoneError:
91
+ pass
92
+
93
+ raise PluginError(
94
+ 'Unable to determine zone identifier for {0} using zone names: {1}'
95
+ .format(domain, domain_name_guesses))
96
+
97
+ def _get_netcup_client(self):
98
+ credentials = self.credentials.conf
99
+ return APIClient(
100
+ credentials('customer-id'),
101
+ credentials('api-key'),
102
+ credentials('api-password'),
103
+ self.conf('login-retries'))
104
+
105
+
106
+ class APIClient:
107
+
108
+ def __init__(self, customer_id, api_key, api_password, login_retries):
109
+ self._customer_id = customer_id
110
+ self._api_key = api_key
111
+ self._api_password = api_password
112
+ self._api_session_id = None
113
+ self._login_retries = login_retries
114
+ if login_retries < 0:
115
+ raise PluginError((
116
+ "Invalid value for --dns-netcup-login-retries: {}. "
117
+ "Must be >= 0."
118
+ ).format(login_retries))
119
+
120
+ # public interface
121
+
122
+ def add_dns_record(self, domain, name, content):
123
+ """Create record."""
124
+ record = _make_record(domain, name, content)
125
+ self._update_records(domain, [record])
126
+
127
+ def delete_dns_record(self, domain, name, content):
128
+ """Delete an existing record. If record does not exist, do nothing."""
129
+ # Note that id/type/hostname/destination are all mandatory when
130
+ # deleting. We must query first to retrieve the record id.
131
+ record = _make_record(domain, name, content)
132
+ queried_records = self._query_records(domain, record)
133
+ self._update_records(domain, [
134
+ dict(record_with_id, deleterecord=True)
135
+ for record_with_id in queried_records
136
+ ])
137
+
138
+ # internal helpers
139
+
140
+ def _query_records(self, domain, record):
141
+ """Query list of record using netcup API."""
142
+ responsedata = self._authenticate_and_call(
143
+ "infoDnsRecords",
144
+ domainname=domain)
145
+ dnsrecords = responsedata.get("dnsrecords", [])
146
+ return [
147
+ retrieved_record
148
+ for retrieved_record in dnsrecords
149
+ if all(retrieved_record[k] == record[k] for k in record)
150
+ ]
151
+
152
+ def _update_records(self, domain, records):
153
+ """Insert or update a list of DNS records.
154
+
155
+ The fields ``hostname``, ``type``, and ``destination`` are mandatory.
156
+ The field ``id`` is mandatory when ``deleterecord=True``
157
+ """
158
+ return self._authenticate_and_call(
159
+ "updateDnsRecords",
160
+ domainname=domain,
161
+ dnsrecordset={"dnsrecords": records})
162
+
163
+ def _authenticate(self, force=False):
164
+ """Authenticate with netcup server. Must be called first."""
165
+
166
+ if force or not self._api_session_id:
167
+ responsedata = _apicall("login", {
168
+ "customernumber": self._customer_id,
169
+ "apikey": self._api_key,
170
+ "apipassword": self._api_password,
171
+ })
172
+
173
+ self._api_session_id = responsedata.get("apisessionid")
174
+ if not self._api_session_id:
175
+ raise PluginError("Login failed due to unknown reason.")
176
+
177
+ return {
178
+ "customernumber": self._customer_id,
179
+ "apikey": self._api_key,
180
+ "apisessionid": self._api_session_id,
181
+ }
182
+
183
+ def _authenticate_and_call(self, action, domainname, **param):
184
+ """Authenticate and then perform API call.
185
+ Auto retry login if session timed out."""
186
+ param = dict(param, domainname=domainname)
187
+ session_auth = self._authenticate(False)
188
+
189
+ for i in range(self._login_retries):
190
+ try:
191
+ return _apicall(action, session_auth, **param)
192
+ except NetcupSessionTimeoutError:
193
+ LOGGER.warning(
194
+ "Login session timed out during call %s for domain %s. "
195
+ "Retrying login (attempt %d)",
196
+ action, domainname, i + 1)
197
+ session_auth = self._authenticate(True)
198
+
199
+ return _apicall(action, session_auth, **param)
200
+
201
+
202
+ def _make_record(domain, name, content):
203
+ name = name.removesuffix('.' + domain)
204
+ return {
205
+ "type": "TXT",
206
+ "hostname": name,
207
+ "destination": content,
208
+ }
209
+
210
+
211
+ def _apicall(action, credentials, **param):
212
+ """Call an API method and return response data. For more info, see:
213
+ https://ccp.netcup.net/run/webservice/servers/endpoint"""
214
+
215
+ LOGGER.debug("_apicall: %s(%s)", action, param.get('domainname', ''))
216
+
217
+ data = {
218
+ "action": action,
219
+ "param": dict(param, **credentials),
220
+ }
221
+ response = requests.post(
222
+ API_ENDPOINT,
223
+ data=json.dumps(data),
224
+ headers={
225
+ "Accept": "application/json",
226
+ "Content-Type": "application/json",
227
+ })
228
+ response.raise_for_status()
229
+ data = response.json()
230
+
231
+ status = data.get("status")
232
+ statuscode = data.get("statuscode")
233
+ shortmessage = data.get("shortmessage")
234
+ longmessage = data.get("longmessage")
235
+
236
+ if status == "success": # statuscode == 2000
237
+ return data.get("responsedata", {})
238
+
239
+ # This happens when the domain is not chosen properly. Full message is:
240
+ # "Value in field domainname does not match requirements of type: domainname"
241
+ if 'Value in field domainname does not match requirements' in longmessage:
242
+ raise NetcupZoneError(
243
+ action, statuscode, shortmessage, longmessage)
244
+
245
+ # The session times out after roughly 30s and fails with this error:
246
+ if 'The session id is not in a valid format.' in longmessage:
247
+ raise NetcupSessionTimeoutError(
248
+ action, statuscode, shortmessage, longmessage)
249
+
250
+ # When something with the JSON format was incorrect, there is also:
251
+ # "Api key missing. JSON decode failed while validating request."
252
+
253
+ raise NetcupError(
254
+ action, statuscode, shortmessage, longmessage)
255
+
256
+
257
+ class NetcupError(PluginError):
258
+
259
+ def __init__(self, action, statuscode, shortmessage, longmessage):
260
+ super().__init__(self._format(
261
+ action, statuscode, shortmessage, longmessage))
262
+ self.action = action
263
+ self.statuscode = statuscode
264
+ self.shortmessage = shortmessage
265
+ self.longmessage = longmessage
266
+
267
+ @classmethod
268
+ def _format(cls, action, statuscode, shortmessage, longmessage):
269
+ return (f'{cls.__name__} during {action}: '
270
+ f'{shortmessage} ({statuscode}) {longmessage}')
271
+
272
+
273
+ class NetcupZoneError(NetcupError):
274
+ pass
275
+
276
+
277
+ class NetcupSessionTimeoutError(NetcupError):
278
+ pass