certbot-dns-dnspod-109 1.0.2__py3-none-any.whl → 1.0.3__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.
- certbot_dns_dnspod_109/_internal/dns_dnspod.py +213 -53
- certbot_dns_dnspod_109/_internal/tests/dns_dnspod_test.py +189 -60
- {certbot_dns_dnspod_109-1.0.2.dist-info → certbot_dns_dnspod_109-1.0.3.dist-info}/METADATA +89 -18
- certbot_dns_dnspod_109-1.0.3.dist-info/RECORD +11 -0
- certbot_dns_dnspod_109-1.0.2.dist-info/RECORD +0 -11
- {certbot_dns_dnspod_109-1.0.2.dist-info → certbot_dns_dnspod_109-1.0.3.dist-info}/LICENSE.txt +0 -0
- {certbot_dns_dnspod_109-1.0.2.dist-info → certbot_dns_dnspod_109-1.0.3.dist-info}/WHEEL +0 -0
- {certbot_dns_dnspod_109-1.0.2.dist-info → certbot_dns_dnspod_109-1.0.3.dist-info}/entry_points.txt +0 -0
- {certbot_dns_dnspod_109-1.0.2.dist-info → certbot_dns_dnspod_109-1.0.3.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""DNS Authenticator for Dnspod."""
|
|
2
2
|
import logging
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, List, Tuple
|
|
4
4
|
from typing import Callable
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
@@ -9,6 +9,8 @@ from certbot.plugins import dns_common
|
|
|
9
9
|
from certbot.plugins.dns_common import CredentialsConfiguration
|
|
10
10
|
|
|
11
11
|
from tencentcloud.common import credential
|
|
12
|
+
from tencentcloud.common.profile.client_profile import ClientProfile
|
|
13
|
+
from tencentcloud.common.profile.http_profile import HttpProfile
|
|
12
14
|
from tencentcloud.dnspod.v20210323 import dnspod_client, models
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
@@ -50,18 +52,20 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|
|
50
52
|
)
|
|
51
53
|
|
|
52
54
|
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
|
53
|
-
self._get_dnspod_client().add_txt_record(
|
|
55
|
+
self._get_dnspod_client().add_txt_record(validation_name, validation, self.ttl)
|
|
54
56
|
|
|
55
57
|
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
|
56
|
-
self._get_dnspod_client().del_txt_record(
|
|
58
|
+
self._get_dnspod_client().del_txt_record(validation_name, validation)
|
|
57
59
|
|
|
58
|
-
def _get_dnspod_client(self) ->
|
|
60
|
+
def _get_dnspod_client(self) -> '_DnspodClient':
|
|
59
61
|
if not self.credentials: # pragma: no cover
|
|
60
62
|
raise errors.Error("Plugin has not been prepared, did you set 'credentials'?")
|
|
61
63
|
if self.credentials.conf('secret_id') and self.credentials.conf('secret_key'):
|
|
62
64
|
return _DnspodClient(secret_id=self.credentials.conf('secret_id'),
|
|
63
65
|
secret_key=self.credentials.conf('secret_key'),
|
|
64
|
-
endpoint=self.credentials.conf('endpoint') or "
|
|
66
|
+
endpoint=self.credentials.conf('endpoint') or "dnspod.tencentcloudapi.com")
|
|
67
|
+
else:
|
|
68
|
+
raise errors.Error("Missing required credentials: secret_id and secret_key are required")
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
class _DnspodClient:
|
|
@@ -69,85 +73,241 @@ class _DnspodClient:
|
|
|
69
73
|
Encapsulates all communication with the Tencent Cloud API 3.0.
|
|
70
74
|
"""
|
|
71
75
|
|
|
72
|
-
def __init__(self, secret_id: str, secret_key: str, endpoint: str) -> None:
|
|
73
|
-
#
|
|
76
|
+
def __init__(self, secret_id: str, secret_key: str, endpoint: str = "dnspod.tencentcloudapi.com") -> None:
|
|
77
|
+
# init Tencent Cloud SDK
|
|
74
78
|
cred = credential.Credential(secret_id, secret_key)
|
|
75
|
-
self.client = dnspod_client.DnspodClient(cred, "")
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
http_profile = HttpProfile()
|
|
81
|
+
http_profile.endpoint = endpoint
|
|
82
|
+
|
|
83
|
+
client_profile = ClientProfile()
|
|
84
|
+
client_profile.httpProfile = http_profile
|
|
85
|
+
|
|
86
|
+
self.client = dnspod_client.DnspodClient(cred, "", client_profile)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _normalize_fqdn(name: str) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Normalizes a fully qualified domain name (FQDN).
|
|
92
|
+
|
|
93
|
+
This method is used to transform an input FQDN by stripping any trailing
|
|
94
|
+
dots and converting the string to lowercase. This ensures uniformity
|
|
95
|
+
and consistent handling of FQDNs for comparisons and other operations.
|
|
96
|
+
|
|
97
|
+
Parameters:
|
|
98
|
+
name: str
|
|
99
|
+
The fully qualified domain name to normalize.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
str
|
|
103
|
+
The normalized FQDN with trailing dots removed and all characters
|
|
104
|
+
in lowercase.
|
|
105
|
+
"""
|
|
106
|
+
return name.rstrip(".").lower()
|
|
107
|
+
|
|
108
|
+
def _to_ascii_fqdn(self, fqdn: str) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Converts a fully qualified domain name (FQDN) to its ASCII representation.
|
|
111
|
+
|
|
112
|
+
This method processes the provided FQDN to ensure it adheres to ASCII encoding
|
|
113
|
+
standards. Non-ASCII labels are converted using IDNA encoding, while ASCII
|
|
114
|
+
labels remain unchanged. Empty labels and invalid input are ignored during
|
|
115
|
+
conversion.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
fqdn: str
|
|
119
|
+
The fully qualified domain name to be processed.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
str
|
|
123
|
+
The ASCII-encoded version of the provided fully qualified domain name.
|
|
124
|
+
"""
|
|
125
|
+
fqdn = self._normalize_fqdn(fqdn)
|
|
126
|
+
labels = fqdn.split(".")
|
|
127
|
+
out = []
|
|
128
|
+
for lab in labels:
|
|
129
|
+
if not lab:
|
|
130
|
+
continue
|
|
131
|
+
if all(ord(c) < 128 for c in lab):
|
|
132
|
+
out.append(lab)
|
|
133
|
+
else:
|
|
134
|
+
out.append(lab.encode("idna").decode("ascii"))
|
|
135
|
+
return ".".join(out)
|
|
136
|
+
|
|
137
|
+
def find_hosted_domain(self, record_fqdn: str) -> Tuple[str, str]:
|
|
138
|
+
"""
|
|
139
|
+
Determines the hosted domain and the subdomain part for a given fully qualified domain name (FQDN).
|
|
140
|
+
|
|
141
|
+
The method searches the list of all hosted domains to find one that either matches exactly
|
|
142
|
+
or is a parent domain to the provided FQDN. If found, it returns the hosted domain and the
|
|
143
|
+
subdomain part of the FQDN. If no matching hosted domain is found, an exception is raised.
|
|
144
|
+
|
|
145
|
+
Parameters:
|
|
146
|
+
record_fqdn (str): The fully qualified domain name (FQDN) to search for.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple[str, str]: A tuple containing:
|
|
150
|
+
- The hosted domain that covers the provided FQDN.
|
|
151
|
+
- The subdomain part of the FQDN relative to the hosted domain
|
|
152
|
+
or "@" if the FQDN matches the hosted domain exactly.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
PluginError: If no hosted domain is found, that can cover the provided FQDN.
|
|
156
|
+
"""
|
|
157
|
+
fqdn = self._to_ascii_fqdn(record_fqdn)
|
|
158
|
+
domains = self._list_all_domain()
|
|
159
|
+
|
|
160
|
+
for domain in domains:
|
|
161
|
+
if fqdn == domain or fqdn.endswith("." + domain):
|
|
162
|
+
if fqdn == domain:
|
|
163
|
+
sub = "@"
|
|
164
|
+
else:
|
|
165
|
+
sub = fqdn[:-(len(domain) + 1)]
|
|
166
|
+
return domain, sub
|
|
167
|
+
|
|
168
|
+
raise errors.PluginError(
|
|
169
|
+
f"Can't find a hosted domain that can cover {record_fqdn}. "
|
|
170
|
+
f"Please make sure you have hosted the domain (e.g. example.com / us.example.com) in DNSPod."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _list_all_domain(self) -> List[str]:
|
|
78
174
|
"""
|
|
79
|
-
|
|
175
|
+
Fetches and returns a sorted list of all domains associated with the account.
|
|
176
|
+
|
|
177
|
+
This method interacts with the API to retrieve a complete list of domains by
|
|
178
|
+
iteratively querying the DescribeDomainList API endpoint. It normalizes and
|
|
179
|
+
filters the retrieved domain data, ensuring that the list contains unique
|
|
180
|
+
entries sorted in descending order by their length.
|
|
80
181
|
|
|
81
|
-
:
|
|
82
|
-
|
|
83
|
-
:param record_content: The record content (typically the challenge validation string).
|
|
84
|
-
:param record_ttl: The record TTL in seconds.
|
|
182
|
+
Returns:
|
|
183
|
+
List[str]: A sorted list of unique domain names.
|
|
85
184
|
"""
|
|
86
|
-
|
|
185
|
+
zones: List[str] = []
|
|
186
|
+
offset = 0
|
|
187
|
+
limit = 3000 # default limit for DescribeDomainList API
|
|
188
|
+
|
|
189
|
+
while True:
|
|
190
|
+
req = models.DescribeDomainListRequest()
|
|
191
|
+
req.Offset = offset
|
|
192
|
+
req.Limit = limit
|
|
193
|
+
resp = self.client.DescribeDomainList(req)
|
|
194
|
+
|
|
195
|
+
domain_list = resp.DomainList or []
|
|
196
|
+
for d in domain_list:
|
|
197
|
+
if getattr(d, "Punycode", None):
|
|
198
|
+
zones.append(self._normalize_fqdn(d.Punycode))
|
|
199
|
+
|
|
200
|
+
total = int(resp.DomainCountInfo.DomainTotal)
|
|
201
|
+
offset += len(domain_list)
|
|
202
|
+
if offset >= total or len(domain_list) == 0:
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
return sorted(set(zones), key=len, reverse=True)
|
|
206
|
+
|
|
207
|
+
def add_txt_record(self, record_name: str, record_content: str, record_ttl: int) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Adds a TXT DNS record to the specified domain using the provided details.
|
|
210
|
+
|
|
211
|
+
This function is responsible for creating a TXT type DNS record in the
|
|
212
|
+
appropriate hosted zone identified by the given record name.
|
|
213
|
+
|
|
214
|
+
Parameters:
|
|
215
|
+
record_name (str): The full name of the record (e.g., "_acme-challenge.example.com").
|
|
216
|
+
record_content (str): The value or content of the TXT record.
|
|
217
|
+
record_ttl (int): The time-to-live (TTL) value for the record, dictating how long
|
|
218
|
+
it can be cached.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
PluginError: If the TXT record addition fails, typically due to an issue in
|
|
222
|
+
the response from the DNS provider.
|
|
223
|
+
|
|
224
|
+
Notes:
|
|
225
|
+
The function automatically determines the hosted domain and subdomain to
|
|
226
|
+
target the correct DNS zone. A log entry is also created upon successful
|
|
227
|
+
record addition with relevant details.
|
|
228
|
+
"""
|
|
229
|
+
zone, subdomain = self.find_hosted_domain(record_name)
|
|
87
230
|
|
|
88
231
|
req = models.CreateTXTRecordRequest()
|
|
89
|
-
req.Domain =
|
|
232
|
+
req.Domain = zone
|
|
233
|
+
req.SubDomain = subdomain
|
|
90
234
|
req.RecordLine = '默认' # required
|
|
91
235
|
req.Value = record_content
|
|
92
236
|
req.TTL = record_ttl
|
|
93
|
-
req.SubDomain = record_name.split(".")[0]
|
|
94
237
|
|
|
95
|
-
|
|
96
|
-
record_id = response.RequestId
|
|
238
|
+
resp = self.client.CreateTXTRecord(req)
|
|
97
239
|
|
|
240
|
+
record_id = getattr(resp, "RecordId", None)
|
|
98
241
|
if not record_id:
|
|
99
|
-
raise
|
|
100
|
-
logger.info(
|
|
242
|
+
raise errors.PluginError(f"Failed to add TXT record, response: {resp}")
|
|
243
|
+
logger.info(
|
|
244
|
+
f"Successfully added TXT record: zone: {zone} sub: {subdomain} record_id: {record_id} request_id: {resp.RequestId}")
|
|
101
245
|
|
|
102
|
-
def del_txt_record(self,
|
|
246
|
+
def del_txt_record(self, record_name: str, record_content: str) -> None:
|
|
103
247
|
"""
|
|
104
|
-
|
|
248
|
+
Deletes a TXT record from the DNS configuration.
|
|
249
|
+
|
|
250
|
+
This method removes a specific TXT record from the DNS configuration associated with the given
|
|
251
|
+
record name and record content. It identifies the domain and subdomain hosting the record,
|
|
252
|
+
verifies its ID, and submits a request to delete it. In the event the record is not found or
|
|
253
|
+
an error occurs during deletion, appropriate logging messages are generated.
|
|
254
|
+
|
|
255
|
+
Parameters:
|
|
256
|
+
record_name: str
|
|
257
|
+
The fully qualified domain name (FQDN) of the TXT record to be deleted.
|
|
258
|
+
record_content: str
|
|
259
|
+
The content of the TXT record to be deleted.
|
|
105
260
|
|
|
106
|
-
:
|
|
107
|
-
|
|
108
|
-
:param record_content: The record content (typically the challenge validation string).
|
|
261
|
+
Raises:
|
|
262
|
+
Exception: If an error occurs during record retrieval or deletion.
|
|
109
263
|
"""
|
|
110
264
|
try:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
record_id = self._find_txt_record_id(domain, record_name, record_content)
|
|
265
|
+
domain, subdomain = self.find_hosted_domain(record_name)
|
|
266
|
+
record_id = self._find_txt_record_id(domain, subdomain, record_content)
|
|
114
267
|
if not record_id:
|
|
115
|
-
logger.warning("Record not found, skipping deletion
|
|
268
|
+
logger.warning("Record not found, skipping deletion: domain=%s sub=%s", domain, subdomain)
|
|
116
269
|
return
|
|
117
270
|
|
|
118
271
|
req = models.DeleteRecordRequest()
|
|
119
272
|
req.Domain = domain
|
|
120
273
|
req.RecordId = record_id
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
logger.info(f"Successfully deleted TXT record: {record_id}, RequestId ID: {response.RequestId}")
|
|
274
|
+
resp = self.client.DeleteRecord(req)
|
|
275
|
+
logger.info(
|
|
276
|
+
f"Successfully deleted TXT record: domain: {domain} sub: {subdomain} request_id: {resp.RequestId}")
|
|
125
277
|
except Exception as e:
|
|
126
|
-
logger.error(
|
|
278
|
+
logger.error("Failed to delete TXT record: %s", e)
|
|
127
279
|
|
|
128
|
-
def _find_txt_record_id(self, domain: str,
|
|
280
|
+
def _find_txt_record_id(self, domain: str, subdomain: str, record_content: str) -> Optional[int]:
|
|
129
281
|
"""
|
|
130
|
-
Find the
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
282
|
+
Find the ID of a TXT record that matches the specified domain name, subdomain,
|
|
283
|
+
and record content.
|
|
284
|
+
|
|
285
|
+
This method searches for an existing TXT record by domain, subdomain, and the
|
|
286
|
+
record content within the list of records returned by the client. If a matching
|
|
287
|
+
record is found, its ID is returned. If no match is found, None is returned.
|
|
288
|
+
|
|
289
|
+
Parameters:
|
|
290
|
+
domain: str
|
|
291
|
+
The domain name in which to search for the TXT record.
|
|
292
|
+
subdomain: str
|
|
293
|
+
The subdomain name for the TXT record, typically the host or name
|
|
294
|
+
portion of the record.
|
|
295
|
+
record_content: str
|
|
296
|
+
The text content of the TXT record that is being searched for.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Optional[int]: The ID of the matching TXT record if found, otherwise None.
|
|
136
300
|
"""
|
|
137
|
-
logger.info(f"Searching for TXT record for {record_name}")
|
|
138
|
-
|
|
139
301
|
req = models.DescribeRecordFilterListRequest()
|
|
140
302
|
req.Domain = domain
|
|
141
|
-
req.SubDomain =
|
|
303
|
+
req.SubDomain = subdomain
|
|
142
304
|
req.RecordType = ["TXT"]
|
|
143
305
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
logger.info(f"Found TXT record for {record.Name}")
|
|
150
|
-
return record.RecordId
|
|
306
|
+
resp = self.client.DescribeRecordFilterList(req)
|
|
307
|
+
for r in (resp.RecordList or []):
|
|
308
|
+
if r.Name == subdomain and r.Value == record_content:
|
|
309
|
+
logger.info(f"Found TXT record for {r.Name}")
|
|
310
|
+
return int(r.RecordId)
|
|
151
311
|
|
|
152
|
-
logger.warning(f"No TXT record found for {
|
|
312
|
+
logger.warning(f"No TXT record found for {subdomain}")
|
|
153
313
|
return None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for
|
|
1
|
+
"""Tests for certbot_dns_dnspod_109._internal.dns_dnspod"""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
import unittest
|
|
@@ -13,7 +13,7 @@ from certbot.plugins.dns_test_common import DOMAIN
|
|
|
13
13
|
from certbot.tests import util as test_util
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
# Simulate
|
|
16
|
+
# Simulate Tencent Cloud API exception for delete-path robustness tests.
|
|
17
17
|
class MockApiException(Exception):
|
|
18
18
|
pass
|
|
19
19
|
|
|
@@ -25,37 +25,39 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
|
|
|
25
25
|
|
|
26
26
|
super().setUp()
|
|
27
27
|
|
|
28
|
-
path = os.path.join(self.tempdir,
|
|
29
|
-
|
|
28
|
+
path = os.path.join(self.tempdir, "file.ini")
|
|
29
|
+
# This plugin version expects prefixed keys in credentials file.
|
|
30
|
+
dns_test_common.write(
|
|
31
|
+
{"dnspod_secret_id": "test_id", "dnspod_secret_key": "test_key"},
|
|
32
|
+
path
|
|
33
|
+
)
|
|
30
34
|
|
|
31
|
-
self.config = mock.MagicMock(
|
|
32
|
-
|
|
35
|
+
self.config = mock.MagicMock(
|
|
36
|
+
dnspod_credentials=path,
|
|
37
|
+
dnspod_propagation_seconds=0,
|
|
38
|
+
)
|
|
33
39
|
|
|
34
40
|
self.auth = Authenticator(self.config, "dnspod")
|
|
35
|
-
|
|
36
41
|
self.mock_client = mock.MagicMock()
|
|
37
|
-
# Mock _get_dnspod_client
|
|
38
|
-
self.auth._get_dnspod_client = mock.MagicMock(return_value=self.mock_client)
|
|
39
42
|
|
|
40
43
|
@test_util.patch_display_util()
|
|
41
44
|
def test_perform(self, unused_mock_get_utility):
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
with mock.patch.object(self.auth, "_get_dnspod_client", return_value=self.mock_client):
|
|
46
|
+
self.auth.perform([self.achall])
|
|
44
47
|
|
|
45
|
-
expected = [mock.call.add_txt_record(
|
|
48
|
+
expected = [mock.call.add_txt_record("_acme-challenge." + DOMAIN, mock.ANY, 600)]
|
|
46
49
|
assert expected == self.mock_client.mock_calls
|
|
47
50
|
|
|
48
51
|
def test_cleanup(self):
|
|
49
|
-
# Test that the cleanup method calls del_txt_record correctly
|
|
50
52
|
self.auth._attempt_cleanup = True
|
|
51
|
-
|
|
53
|
+
with mock.patch.object(self.auth, "_get_dnspod_client", return_value=self.mock_client):
|
|
54
|
+
self.auth.cleanup([self.achall])
|
|
52
55
|
|
|
53
|
-
expected = [mock.call.del_txt_record(
|
|
56
|
+
expected = [mock.call.del_txt_record("_acme-challenge." + DOMAIN, mock.ANY)]
|
|
54
57
|
assert expected == self.mock_client.mock_calls
|
|
55
58
|
|
|
56
59
|
def test_no_creds(self):
|
|
57
|
-
|
|
58
|
-
path = os.path.join(self.tempdir, 'empty.ini')
|
|
60
|
+
path = os.path.join(self.tempdir, "empty.ini")
|
|
59
61
|
dns_test_common.write({}, path)
|
|
60
62
|
self.config.dnspod_credentials = path
|
|
61
63
|
|
|
@@ -65,9 +67,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
|
|
|
65
67
|
auth.perform([self.achall])
|
|
66
68
|
|
|
67
69
|
def test_missing_secret_id(self):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
dns_test_common.write({"secret_key": "test_key"}, path)
|
|
70
|
+
path = os.path.join(self.tempdir, "no_id.ini")
|
|
71
|
+
dns_test_common.write({"dnspod_secret_key": "test_key"}, path)
|
|
71
72
|
self.config.dnspod_credentials = path
|
|
72
73
|
|
|
73
74
|
from certbot_dns_dnspod_109._internal.dns_dnspod import Authenticator
|
|
@@ -76,9 +77,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
|
|
|
76
77
|
auth.perform([self.achall])
|
|
77
78
|
|
|
78
79
|
def test_missing_secret_key(self):
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
dns_test_common.write({"secret_id": "test_id"}, path)
|
|
80
|
+
path = os.path.join(self.tempdir, "no_key.ini")
|
|
81
|
+
dns_test_common.write({"dnspod_secret_id": "test_id"}, path)
|
|
82
82
|
self.config.dnspod_credentials = path
|
|
83
83
|
|
|
84
84
|
from certbot_dns_dnspod_109._internal.dns_dnspod import Authenticator
|
|
@@ -86,115 +86,244 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
|
|
|
86
86
|
with pytest.raises(errors.PluginError):
|
|
87
87
|
auth.perform([self.achall])
|
|
88
88
|
|
|
89
|
+
def test_get_dnspod_client_not_prepared(self):
|
|
90
|
+
self.auth.credentials = None
|
|
91
|
+
with pytest.raises(errors.Error):
|
|
92
|
+
self.auth._get_dnspod_client()
|
|
93
|
+
|
|
94
|
+
def test_get_dnspod_client_missing_required(self):
|
|
95
|
+
creds = mock.MagicMock()
|
|
96
|
+
creds.conf.side_effect = lambda k: {"dnspod_secret_id": "id_only"}.get(k)
|
|
97
|
+
self.auth.credentials = creds
|
|
98
|
+
with pytest.raises(errors.Error):
|
|
99
|
+
self.auth._get_dnspod_client()
|
|
100
|
+
|
|
101
|
+
def test_get_dnspod_client_default_endpoint(self):
|
|
102
|
+
from certbot_dns_dnspod_109._internal import dns_dnspod as mod
|
|
103
|
+
|
|
104
|
+
creds = mock.MagicMock()
|
|
105
|
+
creds.conf.side_effect = lambda k: {
|
|
106
|
+
"secret_id": "id",
|
|
107
|
+
"secret_key": "key",
|
|
108
|
+
"endpoint": None,
|
|
109
|
+
}.get(k)
|
|
110
|
+
|
|
111
|
+
self.auth.credentials = creds
|
|
112
|
+
|
|
113
|
+
with mock.patch.object(mod, "_DnspodClient") as m_client_cls:
|
|
114
|
+
self.auth._get_dnspod_client()
|
|
115
|
+
m_client_cls.assert_called_once_with(
|
|
116
|
+
secret_id="id",
|
|
117
|
+
secret_key="key",
|
|
118
|
+
endpoint="dnspod.tencentcloudapi.com",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def test_get_dnspod_client_custom_endpoint(self):
|
|
122
|
+
from certbot_dns_dnspod_109._internal import dns_dnspod as mod
|
|
123
|
+
|
|
124
|
+
creds = mock.MagicMock()
|
|
125
|
+
creds.conf.side_effect = lambda k: {
|
|
126
|
+
"secret_id": "id",
|
|
127
|
+
"secret_key": "key",
|
|
128
|
+
"endpoint": "custom.endpoint.tencentcloudapi.com",
|
|
129
|
+
}.get(k)
|
|
130
|
+
|
|
131
|
+
self.auth.credentials = creds
|
|
132
|
+
|
|
133
|
+
with mock.patch.object(mod, "_DnspodClient") as m_client_cls:
|
|
134
|
+
self.auth._get_dnspod_client()
|
|
135
|
+
m_client_cls.assert_called_once_with(
|
|
136
|
+
secret_id="id",
|
|
137
|
+
secret_key="key",
|
|
138
|
+
endpoint="custom.endpoint.tencentcloudapi.com",
|
|
139
|
+
)
|
|
140
|
+
|
|
89
141
|
|
|
90
142
|
class DnspodClientTest(unittest.TestCase):
|
|
91
|
-
record_name = "_acme-challenge"
|
|
143
|
+
record_name = "_acme-challenge." + DOMAIN
|
|
92
144
|
record_content = "test_challenge"
|
|
93
145
|
record_ttl = 600
|
|
94
|
-
|
|
146
|
+
zone = DOMAIN
|
|
147
|
+
subdomain = "_acme-challenge"
|
|
95
148
|
record_id = 12345
|
|
96
149
|
|
|
97
150
|
def setUp(self):
|
|
98
151
|
from certbot_dns_dnspod_109._internal.dns_dnspod import _DnspodClient
|
|
99
|
-
|
|
100
|
-
self.mock_dnspod_client = mock.MagicMock()
|
|
101
|
-
self.mock_credential = mock.MagicMock()
|
|
152
|
+
|
|
102
153
|
self.mock_dnspod_sdk_client = mock.MagicMock()
|
|
103
154
|
|
|
104
|
-
# Patch
|
|
105
|
-
patcher = mock.patch(
|
|
106
|
-
|
|
155
|
+
# Patch SDK client constructor.
|
|
156
|
+
patcher = mock.patch(
|
|
157
|
+
"certbot_dns_dnspod_109._internal.dns_dnspod.dnspod_client.DnspodClient",
|
|
158
|
+
return_value=self.mock_dnspod_sdk_client,
|
|
159
|
+
)
|
|
107
160
|
patcher.start()
|
|
108
161
|
self.addCleanup(patcher.stop)
|
|
109
162
|
|
|
110
|
-
# Patch models
|
|
111
|
-
self.models_patcher = mock.patch(
|
|
163
|
+
# Patch request models used by the implementation.
|
|
164
|
+
self.models_patcher = mock.patch("certbot_dns_dnspod_109._internal.dns_dnspod.models")
|
|
112
165
|
self.mock_models = self.models_patcher.start()
|
|
113
166
|
self.addCleanup(self.models_patcher.stop)
|
|
114
167
|
|
|
115
168
|
self.CreateTXTRecordRequest = mock.MagicMock()
|
|
116
169
|
self.DeleteRecordRequest = mock.MagicMock()
|
|
117
170
|
self.DescribeRecordFilterListRequest = mock.MagicMock()
|
|
171
|
+
self.DescribeDomainListRequest = mock.MagicMock()
|
|
118
172
|
|
|
119
173
|
self.mock_models.CreateTXTRecordRequest.return_value = self.CreateTXTRecordRequest
|
|
120
174
|
self.mock_models.DeleteRecordRequest.return_value = self.DeleteRecordRequest
|
|
121
175
|
self.mock_models.DescribeRecordFilterListRequest.return_value = self.DescribeRecordFilterListRequest
|
|
176
|
+
self.mock_models.DescribeDomainListRequest.return_value = self.DescribeDomainListRequest
|
|
177
|
+
|
|
178
|
+
self.client = _DnspodClient(
|
|
179
|
+
secret_id="test_id",
|
|
180
|
+
secret_key="test_key",
|
|
181
|
+
endpoint="dnspod.tencentcloudapi.com",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def test_normalize_fqdn(self):
|
|
185
|
+
# Should strip trailing dots and lowercase.
|
|
186
|
+
assert self.client._normalize_fqdn("WWW.Example.COM.") == "www.example.com"
|
|
187
|
+
|
|
188
|
+
def test_to_ascii_fqdn_ascii_passthrough(self):
|
|
189
|
+
# ASCII labels should remain unchanged except normalization.
|
|
190
|
+
assert self.client._to_ascii_fqdn("WWW.Example.COM.") == "www.example.com"
|
|
191
|
+
|
|
192
|
+
def test_to_ascii_fqdn_idn(self):
|
|
193
|
+
# Non-ASCII labels should be IDNA-encoded.
|
|
194
|
+
out = self.client._to_ascii_fqdn("www.例子.测试.")
|
|
195
|
+
assert out == "www.xn--fsqu00a.xn--0zwm56d"
|
|
196
|
+
|
|
197
|
+
def test_find_hosted_domain_exact_match(self):
|
|
198
|
+
# Exact zone match should return ('zone', '@').
|
|
199
|
+
self.client._list_all_domain = mock.MagicMock(return_value=["example.com"])
|
|
200
|
+
zone, sub = self.client.find_hosted_domain("example.com.")
|
|
201
|
+
assert zone == "example.com"
|
|
202
|
+
assert sub == "@"
|
|
203
|
+
|
|
204
|
+
def test_find_hosted_domain_parent_match(self):
|
|
205
|
+
# Parent zone match should extract left part as subdomain.
|
|
206
|
+
self.client._list_all_domain = mock.MagicMock(return_value=["example.com"])
|
|
207
|
+
zone, sub = self.client.find_hosted_domain("_acme-challenge.example.com.")
|
|
208
|
+
assert zone == "example.com"
|
|
209
|
+
assert sub == "_acme-challenge"
|
|
210
|
+
|
|
211
|
+
def test_find_hosted_domain_longest_suffix_wins(self):
|
|
212
|
+
# Domains are expected to be sorted longest-first; ensure most specific zone is selected.
|
|
213
|
+
self.client._list_all_domain = mock.MagicMock(return_value=["us.example.com", "example.com"])
|
|
214
|
+
zone, sub = self.client.find_hosted_domain("_acme-challenge.us.example.com")
|
|
215
|
+
assert zone == "us.example.com"
|
|
216
|
+
assert sub == "_acme-challenge"
|
|
217
|
+
|
|
218
|
+
def test_find_hosted_domain_not_found(self):
|
|
219
|
+
self.client._list_all_domain = mock.MagicMock(return_value=["example.com"])
|
|
220
|
+
with pytest.raises(errors.PluginError):
|
|
221
|
+
self.client.find_hosted_domain("_acme-challenge.not-hosted.net")
|
|
222
|
+
|
|
223
|
+
def test_list_all_domain_pagination_and_sort(self):
|
|
224
|
+
# Build 2-page API responses and verify dedupe + normalization + sort-by-length-desc.
|
|
225
|
+
d1 = mock.MagicMock()
|
|
226
|
+
d1.Punycode = "Example.COM."
|
|
227
|
+
d2 = mock.MagicMock()
|
|
228
|
+
d2.Punycode = "us.example.com"
|
|
229
|
+
d3 = mock.MagicMock()
|
|
230
|
+
d3.Punycode = "example.com" # duplicate after normalize
|
|
231
|
+
d4 = mock.MagicMock()
|
|
232
|
+
d4.Punycode = None # should be skipped
|
|
122
233
|
|
|
123
|
-
|
|
124
|
-
|
|
234
|
+
resp1 = mock.MagicMock()
|
|
235
|
+
resp1.DomainList = [d1, d2]
|
|
236
|
+
resp1.DomainCountInfo.DomainTotal = 4
|
|
237
|
+
|
|
238
|
+
resp2 = mock.MagicMock()
|
|
239
|
+
resp2.DomainList = [d3, d4]
|
|
240
|
+
resp2.DomainCountInfo.DomainTotal = 4
|
|
241
|
+
|
|
242
|
+
self.mock_dnspod_sdk_client.DescribeDomainList.side_effect = [resp1, resp2]
|
|
243
|
+
|
|
244
|
+
zones = self.client._list_all_domain()
|
|
245
|
+
assert zones == ["us.example.com", "example.com"]
|
|
125
246
|
|
|
126
247
|
def test_add_txt_record(self):
|
|
127
|
-
#
|
|
248
|
+
# Avoid testing zone lookup path here; isolate CreateTXTRecord request behavior.
|
|
249
|
+
self.client.find_hosted_domain = mock.MagicMock(return_value=(self.zone, self.subdomain))
|
|
250
|
+
|
|
128
251
|
mock_response = mock.MagicMock()
|
|
252
|
+
mock_response.RecordId = self.record_id
|
|
129
253
|
mock_response.RequestId = "mock_request_id"
|
|
130
254
|
self.mock_dnspod_sdk_client.CreateTXTRecord.return_value = mock_response
|
|
131
255
|
|
|
132
|
-
self.client.add_txt_record(self.
|
|
133
|
-
self.mock_dnspod_sdk_client.CreateTXTRecord.assert_called_once_with(self.CreateTXTRecordRequest)
|
|
256
|
+
self.client.add_txt_record(self.record_name, self.record_content, self.record_ttl)
|
|
134
257
|
|
|
135
|
-
|
|
136
|
-
assert self.CreateTXTRecordRequest.Domain == self.
|
|
258
|
+
self.mock_dnspod_sdk_client.CreateTXTRecord.assert_called_once_with(self.CreateTXTRecordRequest)
|
|
259
|
+
assert self.CreateTXTRecordRequest.Domain == self.zone
|
|
260
|
+
assert self.CreateTXTRecordRequest.SubDomain == self.subdomain
|
|
137
261
|
assert self.CreateTXTRecordRequest.Value == self.record_content
|
|
138
262
|
assert self.CreateTXTRecordRequest.TTL == self.record_ttl
|
|
139
|
-
assert self.CreateTXTRecordRequest.
|
|
263
|
+
assert self.CreateTXTRecordRequest.RecordLine == "默认"
|
|
140
264
|
|
|
141
265
|
def test_add_txt_record_failed(self):
|
|
142
|
-
|
|
266
|
+
self.client.find_hosted_domain = mock.MagicMock(return_value=(self.zone, self.subdomain))
|
|
267
|
+
|
|
268
|
+
# Missing RecordId should be treated as failure.
|
|
143
269
|
mock_response = mock.MagicMock()
|
|
144
|
-
mock_response.
|
|
270
|
+
mock_response.RecordId = None
|
|
145
271
|
self.mock_dnspod_sdk_client.CreateTXTRecord.return_value = mock_response
|
|
146
272
|
|
|
147
|
-
with pytest.raises(
|
|
148
|
-
self.client.add_txt_record(self.
|
|
273
|
+
with pytest.raises(errors.PluginError):
|
|
274
|
+
self.client.add_txt_record(self.record_name, self.record_content, self.record_ttl)
|
|
149
275
|
|
|
150
276
|
def test_del_txt_record(self):
|
|
151
|
-
|
|
277
|
+
self.client.find_hosted_domain = mock.MagicMock(return_value=(self.zone, self.subdomain))
|
|
278
|
+
|
|
279
|
+
# _find_txt_record_id checks Name == subdomain and Value == content.
|
|
152
280
|
mock_record = mock.MagicMock()
|
|
153
281
|
mock_record.RecordId = self.record_id
|
|
154
|
-
mock_record.Name = self.
|
|
282
|
+
mock_record.Name = self.subdomain
|
|
155
283
|
mock_record.Value = self.record_content
|
|
284
|
+
|
|
156
285
|
mock_response = mock.MagicMock()
|
|
157
286
|
mock_response.RecordList = [mock_record]
|
|
158
287
|
self.mock_dnspod_sdk_client.DescribeRecordFilterList.return_value = mock_response
|
|
159
288
|
|
|
160
|
-
# Simulate the success of deleting records
|
|
161
289
|
mock_del_response = mock.MagicMock()
|
|
162
290
|
mock_del_response.RequestId = "del_request_id"
|
|
163
291
|
self.mock_dnspod_sdk_client.DeleteRecord.return_value = mock_del_response
|
|
164
292
|
|
|
165
|
-
self.client.del_txt_record(self.
|
|
293
|
+
self.client.del_txt_record(self.record_name, self.record_content)
|
|
166
294
|
|
|
167
295
|
self.mock_dnspod_sdk_client.DescribeRecordFilterList.assert_called_once_with(
|
|
168
|
-
self.DescribeRecordFilterListRequest
|
|
296
|
+
self.DescribeRecordFilterListRequest
|
|
297
|
+
)
|
|
169
298
|
self.mock_dnspod_sdk_client.DeleteRecord.assert_called_once_with(self.DeleteRecordRequest)
|
|
170
|
-
|
|
171
299
|
assert self.DeleteRecordRequest.RecordId == self.record_id
|
|
172
|
-
assert self.DeleteRecordRequest.Domain == self.
|
|
300
|
+
assert self.DeleteRecordRequest.Domain == self.zone
|
|
173
301
|
|
|
174
302
|
def test_del_txt_record_not_found(self):
|
|
175
|
-
|
|
303
|
+
self.client.find_hosted_domain = mock.MagicMock(return_value=(self.zone, self.subdomain))
|
|
304
|
+
|
|
176
305
|
mock_response = mock.MagicMock()
|
|
177
306
|
mock_response.RecordList = []
|
|
178
307
|
self.mock_dnspod_sdk_client.DescribeRecordFilterList.return_value = mock_response
|
|
179
308
|
|
|
180
|
-
#
|
|
181
|
-
self.client.del_txt_record(self.
|
|
309
|
+
# Not found should be a no-op.
|
|
310
|
+
self.client.del_txt_record(self.record_name, self.record_content)
|
|
182
311
|
self.mock_dnspod_sdk_client.DeleteRecord.assert_not_called()
|
|
183
312
|
|
|
184
313
|
def test_del_txt_record_error(self):
|
|
185
|
-
|
|
314
|
+
self.client.find_hosted_domain = mock.MagicMock(return_value=(self.zone, self.subdomain))
|
|
315
|
+
|
|
186
316
|
mock_record = mock.MagicMock()
|
|
187
317
|
mock_record.RecordId = self.record_id
|
|
188
|
-
mock_record.Name = self.
|
|
318
|
+
mock_record.Name = self.subdomain
|
|
189
319
|
mock_record.Value = self.record_content
|
|
190
320
|
mock_response = mock.MagicMock()
|
|
191
321
|
mock_response.RecordList = [mock_record]
|
|
192
322
|
self.mock_dnspod_sdk_client.DescribeRecordFilterList.return_value = mock_response
|
|
193
323
|
|
|
324
|
+
# del_txt_record should swallow exceptions and log errors.
|
|
194
325
|
self.mock_dnspod_sdk_client.DeleteRecord.side_effect = MockApiException("Delete error")
|
|
195
|
-
|
|
196
|
-
# Should not throw exceptions, but errors will be logged
|
|
197
|
-
self.client.del_txt_record(self.domain, self.record_name, self.record_content)
|
|
326
|
+
self.client.del_txt_record(self.record_name, self.record_content)
|
|
198
327
|
|
|
199
328
|
|
|
200
329
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: certbot-dns-dnspod-109
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Dnspod DNS Authenticator plugin for Certbot
|
|
5
5
|
Home-page: https://github.com/10935336/certbot-dns-dnspod-109
|
|
6
6
|
Author: 10935336
|
|
@@ -43,6 +43,9 @@ Requires-Dist: pytest; extra == "test"
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
## Just Another DNSPod DNS Authenticator plugin for Certbot
|
|
46
|
+
|
|
47
|
+
Just Another Tencent Cloud DNSPod DNS Authenticator plugin for [Certbot](https://certbot.eff.org/)
|
|
48
|
+
|
|
46
49
|
The `certbot-dns-dnspod-109` plugin automates the process of
|
|
47
50
|
completing a `dns-01` challenge (`~acme.challenges.DNS01`)
|
|
48
51
|
by creating, and subsequently removing, TXT records using the
|
|
@@ -54,6 +57,8 @@ then this is your plugin.
|
|
|
54
57
|
|
|
55
58
|
Tested on
|
|
56
59
|
- Certbot 3.0.1
|
|
60
|
+
- Certbot 5.1.0
|
|
61
|
+
- Certbot 5.2.2
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
## Usage
|
|
@@ -71,6 +76,8 @@ pip install git+https://github.com/10935336/certbot-dns-dnspod-109.git
|
|
|
71
76
|
|
|
72
77
|
|
|
73
78
|
#### snap:
|
|
79
|
+
The snap version is deprecated and will not receive updates after version v1.0.2.
|
|
80
|
+
|
|
74
81
|
```bash
|
|
75
82
|
snap install certbot-dns-dnspod-10935336
|
|
76
83
|
snap set certbot trust-plugin-with-root=ok
|
|
@@ -91,6 +98,11 @@ dns_dnspod_109_secret_id=foo
|
|
|
91
98
|
dns_dnspod_109_secret_key=bar
|
|
92
99
|
```
|
|
93
100
|
|
|
101
|
+
Optional parameters:
|
|
102
|
+
```ini
|
|
103
|
+
dns_dnspod_109_endpoint=dnspod.tencentcloudapi.com
|
|
104
|
+
```
|
|
105
|
+
|
|
94
106
|
### 4. Ready to go
|
|
95
107
|
|
|
96
108
|
#### Usage Examples
|
|
@@ -113,6 +125,15 @@ certbot certonly \
|
|
|
113
125
|
-d www.example.com
|
|
114
126
|
```
|
|
115
127
|
|
|
128
|
+
Obtain a wildcard certificate for `example.com`
|
|
129
|
+
```bash
|
|
130
|
+
certbot certonly \
|
|
131
|
+
-a dns-dnspod-109 \
|
|
132
|
+
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
133
|
+
-d example.com \
|
|
134
|
+
-d *.example.com
|
|
135
|
+
```
|
|
136
|
+
|
|
116
137
|
To acquire a certificate for example.com, waiting 60 seconds for DNS propagation
|
|
117
138
|
```bash
|
|
118
139
|
certbot certonly \
|
|
@@ -122,7 +143,7 @@ certbot certonly \
|
|
|
122
143
|
-d example.com
|
|
123
144
|
```
|
|
124
145
|
|
|
125
|
-
Test run
|
|
146
|
+
Test run (Skipping the final certificate issuance)
|
|
126
147
|
```bash
|
|
127
148
|
certbot certonly \
|
|
128
149
|
--register-unsafely-without-email \
|
|
@@ -133,8 +154,22 @@ certbot certonly \
|
|
|
133
154
|
--dry-run
|
|
134
155
|
```
|
|
135
156
|
|
|
157
|
+
Test run a wildcard certificate for `example.com`(Skipping the final certificate issuance)
|
|
158
|
+
```bash
|
|
159
|
+
certbot certonly \
|
|
160
|
+
--register-unsafely-without-email \
|
|
161
|
+
-a dns-dnspod-109 \
|
|
162
|
+
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
163
|
+
--dns-dnspod-109-propagation-seconds 60 \
|
|
164
|
+
-v \
|
|
165
|
+
--dry-run \
|
|
166
|
+
-d example.com \
|
|
167
|
+
-d *.example.com
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
|
|
136
171
|
## Parameter Description
|
|
137
|
-
``--dns-dnspod-109-credentials <path>``
|
|
172
|
+
``--dns-dnspod-109-credentials <path>`` Path to the credential file (required)
|
|
138
173
|
|
|
139
174
|
``--dns-dnspod-109-propagation-seconds`` The number of seconds to wait for DNS propagation before asking the ACME server to verify DNS records. If DNS records appear to be added successfully but verification fails, try increasing this value. (Default: 10)
|
|
140
175
|
|
|
@@ -151,12 +186,16 @@ certbot certonly \
|
|
|
151
186
|
|
|
152
187
|
## 只是另一个适用于 Certbot 的 DNSPod DNS Authenticator 插件
|
|
153
188
|
|
|
189
|
+
只是另一个适用于 [Certbot](https://certbot.eff.org/) 的 Tencent Cloud DNSPod DNS Authenticator 插件
|
|
190
|
+
|
|
154
191
|
`certbot-dns-dnspod-109` 插件通过使用 Dnspod API(腾讯云 API 3.0)创建并随后删除 TXT 记录,自动完成`dns-01` 质询(`~acme.challenges.DNS01`)。
|
|
155
192
|
|
|
156
193
|
如果你使用 [Dnspod](https://www.dnspod.cn/) ([腾讯云](https://cloud.tencent.com)) 作为你的域名解析服务提供商,那么这就是你的插件。
|
|
157
194
|
|
|
158
195
|
在以下版本中测试通过
|
|
159
196
|
- Certbot 3.0.1
|
|
197
|
+
- Certbot 5.1.0
|
|
198
|
+
- Certbot 5.2.2
|
|
160
199
|
|
|
161
200
|
## 使用方法
|
|
162
201
|
|
|
@@ -172,6 +211,9 @@ pip install git+https://github.com/10935336/certbot-dns-dnspod-109.git
|
|
|
172
211
|
```
|
|
173
212
|
|
|
174
213
|
#### snap:
|
|
214
|
+
|
|
215
|
+
snap 版本已弃用,将不会收到 v1.0.2 版本之后的更新
|
|
216
|
+
|
|
175
217
|
```bash
|
|
176
218
|
snap install certbot-dns-dnspod-10935336
|
|
177
219
|
snap set certbot trust-plugin-with-root=ok
|
|
@@ -186,12 +228,17 @@ snap connect certbot:plugin certbot-dns-dnspod-10935336
|
|
|
186
228
|
|
|
187
229
|
### 3. 准备凭证文件
|
|
188
230
|
|
|
189
|
-
foobar.ini:
|
|
231
|
+
foobar.ini:
|
|
190
232
|
```ini
|
|
191
233
|
dns_dnspod_109_secret_id=foo
|
|
192
234
|
dns_dnspod_109_secret_key=bar
|
|
193
235
|
```
|
|
194
236
|
|
|
237
|
+
可选参数:
|
|
238
|
+
```ini
|
|
239
|
+
dns_dnspod_109_endpoint=dnspod.tencentcloudapi.com
|
|
240
|
+
```
|
|
241
|
+
|
|
195
242
|
### 4. 准备就绪
|
|
196
243
|
|
|
197
244
|
#### 使用示例
|
|
@@ -200,30 +247,40 @@ dns_dnspod_109_secret_key=bar
|
|
|
200
247
|
|
|
201
248
|
```bash
|
|
202
249
|
certbot certonly \
|
|
203
|
-
-a dns-dnspod-109 \
|
|
204
|
-
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
205
|
-
-d example.com
|
|
250
|
+
-a dns-dnspod-109 \
|
|
251
|
+
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
252
|
+
-d example.com
|
|
206
253
|
```
|
|
207
254
|
|
|
208
255
|
获取同时有 `example.com` 和 `www.example.com` 的单个证书
|
|
209
256
|
```bash
|
|
210
257
|
certbot certonly \
|
|
211
|
-
-a dns-dnspod-109 \
|
|
212
|
-
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
213
|
-
-d example.com \
|
|
214
|
-
-d www.example.com
|
|
258
|
+
-a dns-dnspod-109 \
|
|
259
|
+
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
260
|
+
-d example.com \
|
|
261
|
+
-d www.example.com
|
|
215
262
|
```
|
|
216
263
|
|
|
264
|
+
获取 `example.com` 的泛域名证书
|
|
265
|
+
```bash
|
|
266
|
+
certbot certonly \
|
|
267
|
+
-a dns-dnspod-109 \
|
|
268
|
+
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
269
|
+
-d example.com \
|
|
270
|
+
-d *.example.com
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
|
|
217
274
|
获取 `example.com` 的证书,但设置等待 60 秒(等待 DNS 传播)
|
|
218
275
|
```bash
|
|
219
276
|
certbot certonly \
|
|
220
|
-
-a dns-dnspod-109 \
|
|
221
|
-
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
222
|
-
--dns-dnspod-109-propagation-seconds 60 \
|
|
223
|
-
-d example.com
|
|
277
|
+
-a dns-dnspod-109 \
|
|
278
|
+
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
279
|
+
--dns-dnspod-109-propagation-seconds 60 \
|
|
280
|
+
-d example.com
|
|
224
281
|
```
|
|
225
282
|
|
|
226
|
-
|
|
283
|
+
测试运行(跳过最终证书颁发)
|
|
227
284
|
```bash
|
|
228
285
|
certbot certonly \
|
|
229
286
|
--register-unsafely-without-email \
|
|
@@ -234,8 +291,22 @@ certbot certonly \
|
|
|
234
291
|
--dry-run
|
|
235
292
|
```
|
|
236
293
|
|
|
294
|
+
测试运行,获取 `example.com` 的泛域名证书(跳过最终证书颁发)
|
|
295
|
+
```bash
|
|
296
|
+
certbot certonly \
|
|
297
|
+
--register-unsafely-without-email \
|
|
298
|
+
-a dns-dnspod-109 \
|
|
299
|
+
--dns-dnspod-109-credentials ~/.secrets/certbot/dnspod-109.ini \
|
|
300
|
+
--dns-dnspod-109-propagation-seconds 60 \
|
|
301
|
+
-v \
|
|
302
|
+
--dry-run \
|
|
303
|
+
-d example.com \
|
|
304
|
+
-d *.example.com
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
|
|
237
308
|
## 参数说明
|
|
238
|
-
``--dns-dnspod-109-credentials <路径>``
|
|
309
|
+
``--dns-dnspod-109-credentials <路径>`` 指向凭证文件的路径(必需)
|
|
239
310
|
|
|
240
|
-
``--dns-dnspod-109-propagation-seconds`` 在要求 ACME 服务器验证 DNS 记录之前等待 DNS 传播的秒数。如果显示 DNS 记录添加成功但验证失败,则尝试增加此值
|
|
311
|
+
``--dns-dnspod-109-propagation-seconds`` 在要求 ACME 服务器验证 DNS 记录之前等待 DNS 传播的秒数。如果显示 DNS 记录添加成功但验证失败,则尝试增加此值 (默认值:10)
|
|
241
312
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
certbot_dns_dnspod_109/__init__.py,sha256=qvnjZ72P7cvpavn66_Ws2c7s0Ft6OJpaSiIP8bAtYmc,3479
|
|
2
|
+
certbot_dns_dnspod_109/_internal/__init__.py,sha256=TxsS2fQuUdzQP-YoerMhKrCrodqACPybbe2JDGdtcZw,95
|
|
3
|
+
certbot_dns_dnspod_109/_internal/dns_dnspod.py,sha256=SXBWvXr7NaifW-WE-LYF3jYqqdh2h8hwHnjPG5XlcOU,12850
|
|
4
|
+
certbot_dns_dnspod_109/_internal/tests/__init__.py,sha256=rjj8qB-_GHbjCMAlaq6MKyzVtE_NWf5A6NGYGC8JAPk,68
|
|
5
|
+
certbot_dns_dnspod_109/_internal/tests/dns_dnspod_test.py,sha256=56PwuKH6lGdLhZzI3DrHmEDJqyZWqaCsknrd9_-hqB4,13510
|
|
6
|
+
certbot_dns_dnspod_109-1.0.3.dist-info/LICENSE.txt,sha256=LMXecVrlqXbwhvukkwiAyeQhkUFz_IURG_9RTUdXEj8,10786
|
|
7
|
+
certbot_dns_dnspod_109-1.0.3.dist-info/METADATA,sha256=DEObA_RWSXXhPeHReRI6a3BQoyYWrnBTO2UyP_Uc3yk,8719
|
|
8
|
+
certbot_dns_dnspod_109-1.0.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
9
|
+
certbot_dns_dnspod_109-1.0.3.dist-info/entry_points.txt,sha256=ri1pHfGGa7B3kLCWkTGn8Xg32rdNTLqBv4pDHwD8OUs,93
|
|
10
|
+
certbot_dns_dnspod_109-1.0.3.dist-info/top_level.txt,sha256=TZ8F7YwXuC4Q35y9QMVXnIAAf2aEEhxyBCQVwIK5yvg,23
|
|
11
|
+
certbot_dns_dnspod_109-1.0.3.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
certbot_dns_dnspod_109/__init__.py,sha256=qvnjZ72P7cvpavn66_Ws2c7s0Ft6OJpaSiIP8bAtYmc,3479
|
|
2
|
-
certbot_dns_dnspod_109/_internal/__init__.py,sha256=TxsS2fQuUdzQP-YoerMhKrCrodqACPybbe2JDGdtcZw,95
|
|
3
|
-
certbot_dns_dnspod_109/_internal/dns_dnspod.py,sha256=nNz-Fyve_1an-D-cMthvV2IcswrrffU-M5hZ2cB6KGM,6393
|
|
4
|
-
certbot_dns_dnspod_109/_internal/tests/__init__.py,sha256=rjj8qB-_GHbjCMAlaq6MKyzVtE_NWf5A6NGYGC8JAPk,68
|
|
5
|
-
certbot_dns_dnspod_109/_internal/tests/dns_dnspod_test.py,sha256=6J7Rgk7iEeW9J_32bpIx4wmQ9aiyyi7koCNXBbj5fTo,8864
|
|
6
|
-
certbot_dns_dnspod_109-1.0.2.dist-info/LICENSE.txt,sha256=LMXecVrlqXbwhvukkwiAyeQhkUFz_IURG_9RTUdXEj8,10786
|
|
7
|
-
certbot_dns_dnspod_109-1.0.2.dist-info/METADATA,sha256=VgBZIy_c4Dzu_GhEGF2fJpRSgHzwM0AIP60HS1_6vkA,6853
|
|
8
|
-
certbot_dns_dnspod_109-1.0.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
9
|
-
certbot_dns_dnspod_109-1.0.2.dist-info/entry_points.txt,sha256=ri1pHfGGa7B3kLCWkTGn8Xg32rdNTLqBv4pDHwD8OUs,93
|
|
10
|
-
certbot_dns_dnspod_109-1.0.2.dist-info/top_level.txt,sha256=TZ8F7YwXuC4Q35y9QMVXnIAAf2aEEhxyBCQVwIK5yvg,23
|
|
11
|
-
certbot_dns_dnspod_109-1.0.2.dist-info/RECORD,,
|
{certbot_dns_dnspod_109-1.0.2.dist-info → certbot_dns_dnspod_109-1.0.3.dist-info}/LICENSE.txt
RENAMED
|
File without changes
|
|
File without changes
|
{certbot_dns_dnspod_109-1.0.2.dist-info → certbot_dns_dnspod_109-1.0.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{certbot_dns_dnspod_109-1.0.2.dist-info → certbot_dns_dnspod_109-1.0.3.dist-info}/top_level.txt
RENAMED
|
File without changes
|