ddns 4.0.1__py2.py3-none-any.whl → 4.1.0b1__py2.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.
Potentially problematic release.
This version of ddns might be problematic. Click here for more details.
- ddns/__builtins__.pyi +5 -0
- ddns/__init__.py +6 -4
- ddns/__main__.py +75 -69
- ddns/provider/__init__.py +59 -0
- ddns/provider/_base.py +659 -0
- ddns/provider/alidns.py +127 -176
- ddns/provider/callback.py +67 -105
- ddns/provider/cloudflare.py +87 -151
- ddns/provider/debug.py +20 -0
- ddns/provider/dnscom.py +91 -169
- ddns/provider/dnspod.py +105 -174
- ddns/provider/dnspod_com.py +11 -8
- ddns/provider/he.py +41 -78
- ddns/provider/huaweidns.py +133 -256
- ddns/provider/tencentcloud.py +195 -0
- ddns/util/cache.py +67 -54
- ddns/util/config.py +29 -32
- ddns/util/http.py +277 -0
- ddns/util/ip.py +20 -17
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/METADATA +96 -71
- ddns-4.1.0b1.dist-info/RECORD +26 -0
- ddns-4.0.1.dist-info/RECORD +0 -21
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/WHEEL +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/entry_points.txt +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/top_level.txt +0 -0
ddns/provider/huaweidns.py
CHANGED
|
@@ -2,264 +2,141 @@
|
|
|
2
2
|
"""
|
|
3
3
|
HuaweiDNS API
|
|
4
4
|
华为DNS解析操作库
|
|
5
|
-
|
|
6
|
-
@author: cybmp3
|
|
5
|
+
@author: NewFuture
|
|
7
6
|
"""
|
|
8
7
|
|
|
8
|
+
from ._base import BaseProvider, TYPE_JSON, hmac_sha256_authorization, sha256_hash, join_domain
|
|
9
|
+
from json import dumps as jsonencode
|
|
10
|
+
from time import strftime, gmtime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HuaweiDNSProvider(BaseProvider):
|
|
14
|
+
API = "https://dns.myhuaweicloud.com"
|
|
15
|
+
content_type = TYPE_JSON
|
|
16
|
+
algorithm = "SDK-HMAC-SHA256"
|
|
17
|
+
|
|
18
|
+
def _validate(self):
|
|
19
|
+
self.logger.warning(
|
|
20
|
+
"华为云 DNS 缺少充分的真实环境测试,请及时在 GitHub Issues 中反馈: %s",
|
|
21
|
+
"https://github.com/NewFuture/DDNS/issues",
|
|
22
|
+
)
|
|
23
|
+
super(HuaweiDNSProvider, self)._validate()
|
|
24
|
+
|
|
25
|
+
def _request(self, method, path, **params):
|
|
26
|
+
"""
|
|
27
|
+
https://support.huaweicloud.com/api-dns/zh-cn_topic_0037134406.html
|
|
28
|
+
https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-002.html
|
|
29
|
+
https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-003.html
|
|
30
|
+
https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-004.html
|
|
31
|
+
"""
|
|
32
|
+
# type: (str, str, **Any) -> dict
|
|
33
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
34
|
+
if method.upper() == "GET" or method.upper() == "DELETE":
|
|
35
|
+
query = self._encode(sorted(params.items()))
|
|
36
|
+
body = ""
|
|
37
|
+
else:
|
|
38
|
+
query = ""
|
|
39
|
+
body = jsonencode(params)
|
|
40
|
+
|
|
41
|
+
now = strftime("%Y%m%dT%H%M%SZ", gmtime())
|
|
42
|
+
headers = {
|
|
43
|
+
"content-type": self.content_type,
|
|
44
|
+
"host": self.API.split("://", 1)[1].strip("/"),
|
|
45
|
+
"X-Sdk-Date": now,
|
|
46
|
+
}
|
|
9
47
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class Config:
|
|
34
|
-
ID = "id" # AK
|
|
35
|
-
TOKEN = "TOKEN" # AS
|
|
36
|
-
PROXY = None # 代理设置
|
|
37
|
-
TTL = None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class API:
|
|
41
|
-
# API 配置
|
|
42
|
-
SCHEME = 'https'
|
|
43
|
-
SITE = 'dns.myhuaweicloud.com' # API endpoint
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def HexEncodeSHA256Hash(data):
|
|
47
|
-
sha = sha256()
|
|
48
|
-
sha.update(data)
|
|
49
|
-
return sha.hexdigest()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def StringToSign(canonical_request, t):
|
|
53
|
-
b = HexEncodeSHA256Hash(canonical_request)
|
|
54
|
-
return "%s\n%s\n%s" % (Algorithm, datetime.strftime(t, BasicDateFormat), b)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def CanonicalHeaders(headers, signed_headers):
|
|
58
|
-
a = []
|
|
59
|
-
__headers = {}
|
|
60
|
-
for key in headers:
|
|
61
|
-
key_encoded = key.lower()
|
|
62
|
-
value = headers[key]
|
|
63
|
-
value_encoded = value.strip()
|
|
64
|
-
__headers[key_encoded] = value_encoded
|
|
65
|
-
for key in signed_headers:
|
|
66
|
-
a.append(key + ":" + __headers[key])
|
|
67
|
-
return '\n'.join(a) + "\n"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def request(method, path, param=None, body=None, **params):
|
|
71
|
-
# path 是不带host但是 前面需要带 / , body json 字符串或者自己从dict转换下
|
|
72
|
-
# 也可以自己改成 判断下是不是post 是post params就是body
|
|
73
|
-
if param:
|
|
74
|
-
params.update(param)
|
|
75
|
-
|
|
76
|
-
query = urlencode(sorted(params.items()))
|
|
77
|
-
headers = {"content-type": "application/json"} # 初始化header
|
|
78
|
-
headers["X-Sdk-Date"] = datetime.strftime(
|
|
79
|
-
datetime.utcnow(), BasicDateFormat)
|
|
80
|
-
headers["host"] = API.SITE
|
|
81
|
-
# 如何后来有需要把header头 key转换为小写 value 删除前导空格和尾随空格
|
|
82
|
-
sign_headers = []
|
|
83
|
-
for key in headers:
|
|
84
|
-
sign_headers.append(key.lower())
|
|
85
|
-
# 先排序
|
|
86
|
-
sign_headers.sort()
|
|
87
|
-
|
|
88
|
-
if body is None:
|
|
89
|
-
body = ""
|
|
90
|
-
|
|
91
|
-
hex_encode = HexEncodeSHA256Hash(body.encode('utf-8'))
|
|
92
|
-
# 生成文档中的CanonicalRequest
|
|
93
|
-
canonical_headers = CanonicalHeaders(headers, sign_headers)
|
|
94
|
-
|
|
95
|
-
# 签名中的path 必须 / 结尾
|
|
96
|
-
if path[-1] != '/':
|
|
97
|
-
sign_path = path + "/"
|
|
98
|
-
else:
|
|
99
|
-
sign_path = path
|
|
100
|
-
|
|
101
|
-
canonical_request = "%s\n%s\n%s\n%s\n%s\n%s" % (method.upper(), sign_path, query,
|
|
102
|
-
canonical_headers, ";".join(sign_headers), hex_encode)
|
|
103
|
-
|
|
104
|
-
hashed_canonical_request = HexEncodeSHA256Hash(
|
|
105
|
-
canonical_request.encode('utf-8'))
|
|
106
|
-
|
|
107
|
-
# StringToSign
|
|
108
|
-
str_to_sign = "%s\n%s\n%s" % (
|
|
109
|
-
Algorithm, headers['X-Sdk-Date'], hashed_canonical_request)
|
|
110
|
-
|
|
111
|
-
secret = Config.TOKEN
|
|
112
|
-
# 计算签名 HexEncode(HMAC(Access Secret Key, string to sign))
|
|
113
|
-
signature = hmac(secret.encode(
|
|
114
|
-
'utf-8'), str_to_sign.encode('utf-8'), digestmod=sha256).digest()
|
|
115
|
-
signature = hexlify(signature).decode()
|
|
116
|
-
# 添加签名信息到请求头
|
|
117
|
-
auth_header = "%s Access=%s, SignedHeaders=%s, Signature=%s" % (
|
|
118
|
-
Algorithm, Config.ID, ";".join(sign_headers), signature)
|
|
119
|
-
headers['Authorization'] = auth_header
|
|
120
|
-
# 创建Http请求
|
|
121
|
-
|
|
122
|
-
if Config.PROXY:
|
|
123
|
-
conn = HTTPSConnection(Config.PROXY)
|
|
124
|
-
conn.set_tunnel(API.SITE, 443)
|
|
125
|
-
else:
|
|
126
|
-
conn = HTTPSConnection(API.SITE)
|
|
127
|
-
conn.request(method, API.SCHEME + "://" + API.SITE +
|
|
128
|
-
path + '?' + query, body, headers)
|
|
129
|
-
info(API.SCHEME + "://" + API.SITE + path + '?' + query, body)
|
|
130
|
-
resp = conn.getresponse()
|
|
131
|
-
data = resp.read().decode('utf8')
|
|
132
|
-
resp.close()
|
|
133
|
-
if resp.status < 200 or resp.status >= 300:
|
|
134
|
-
|
|
135
|
-
warning('%s : error[%d]: %s', path, resp.status, data)
|
|
136
|
-
raise Exception(data)
|
|
137
|
-
else:
|
|
138
|
-
data = jsondecode(data)
|
|
139
|
-
debug('%s : result:%s', path, data)
|
|
48
|
+
# 使用通用签名函数
|
|
49
|
+
body_hash = sha256_hash(body)
|
|
50
|
+
# 华为云需要在签名时使用带尾斜杠的路径
|
|
51
|
+
sign_path = path if path.endswith("/") else path + "/"
|
|
52
|
+
authorization_format = "%s Access=%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
|
|
53
|
+
self.algorithm,
|
|
54
|
+
self.auth_id,
|
|
55
|
+
)
|
|
56
|
+
authorization = hmac_sha256_authorization(
|
|
57
|
+
secret_key=self.auth_token,
|
|
58
|
+
method=method,
|
|
59
|
+
path=sign_path,
|
|
60
|
+
query=query,
|
|
61
|
+
headers=headers,
|
|
62
|
+
body_hash=body_hash,
|
|
63
|
+
signing_string_format=self.algorithm + "\n" + now + "\n{HashedCanonicalRequest}",
|
|
64
|
+
authorization_format=authorization_format,
|
|
65
|
+
)
|
|
66
|
+
headers["Authorization"] = authorization
|
|
67
|
+
|
|
68
|
+
# 使用原始路径发送实际请求
|
|
69
|
+
path = "{}?{}".format(path, query) if query else path
|
|
70
|
+
data = self._http(method, path, headers=headers, body=body)
|
|
140
71
|
return data
|
|
141
72
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if records: # update
|
|
213
|
-
for (rid, record) in records.items():
|
|
214
|
-
if record['records'] != value:
|
|
215
|
-
"""
|
|
216
|
-
PUT https://{endpoint}/v2/zones/{zone_id}/recordsets/{recordset_id}
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
"name" : "www.example.com.",
|
|
220
|
-
"description" : "This is an example record set.",
|
|
221
|
-
"type" : "A",
|
|
222
|
-
"ttl" : 3600,
|
|
223
|
-
"records" : [ "192.168.10.1", "192.168.10.2" ]
|
|
224
|
-
}
|
|
225
|
-
"""
|
|
226
|
-
body = {
|
|
227
|
-
"name": domain,
|
|
228
|
-
"description": "Managed by DDNS.",
|
|
229
|
-
"type": record_type,
|
|
230
|
-
"records": [
|
|
231
|
-
value
|
|
232
|
-
]
|
|
233
|
-
}
|
|
234
|
-
# 如果TTL不为空,则添加到字典中
|
|
235
|
-
if Config.TTL is not None:
|
|
236
|
-
body['ttl'] = Config.TTL
|
|
237
|
-
res = request('PUT', '/v2/zones/' + zoneid + '/recordsets/' + record['id'],
|
|
238
|
-
body=str(jsonencode(body)))
|
|
239
|
-
if res:
|
|
240
|
-
get_records.records[cache_key][rid]['records'] = value
|
|
241
|
-
result[rid] = res.get("name")
|
|
242
|
-
else:
|
|
243
|
-
result[rid] = "Update fail!\n" + str(res)
|
|
244
|
-
else:
|
|
245
|
-
result[rid] = domain
|
|
246
|
-
else: # create
|
|
247
|
-
body = {
|
|
248
|
-
"name": domain,
|
|
249
|
-
"description": "Managed by DDNS.",
|
|
250
|
-
"type": record_type,
|
|
251
|
-
"records": [
|
|
252
|
-
value
|
|
253
|
-
]
|
|
254
|
-
}
|
|
255
|
-
# 如果TTL不为空,则添加到字典中
|
|
256
|
-
if Config.TTL is not None:
|
|
257
|
-
body['ttl'] = Config.TTL
|
|
258
|
-
res = request('POST', '/v2/zones/' + zoneid + '/recordsets',
|
|
259
|
-
body=str(jsonencode(body)))
|
|
260
|
-
if res:
|
|
261
|
-
get_records.records[cache_key][res['id']] = res
|
|
262
|
-
result = res
|
|
263
|
-
else:
|
|
264
|
-
result = domain + " created fail!"
|
|
265
|
-
return result
|
|
73
|
+
def _query_zone_id(self, domain):
|
|
74
|
+
"""https://support.huaweicloud.com/api-dns/dns_api_62003.html"""
|
|
75
|
+
domain = domain + "." if not domain.endswith(".") else domain
|
|
76
|
+
data = self._request("GET", "/v2/zones", search_mode="equal", limit=500, name=domain)
|
|
77
|
+
zones = data.get("zones", [])
|
|
78
|
+
zone = next((z for z in zones if domain == z.get("name")), None)
|
|
79
|
+
zoneid = zone and zone["id"]
|
|
80
|
+
return zoneid
|
|
81
|
+
|
|
82
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
83
|
+
"""
|
|
84
|
+
v2.1 https://support.huaweicloud.com/api-dns/dns_api_64004.html
|
|
85
|
+
v2 https://support.huaweicloud.com/api-dns/ListRecordSetsByZone.html
|
|
86
|
+
"""
|
|
87
|
+
domain = join_domain(subdomain, main_domain) + "."
|
|
88
|
+
data = self._request(
|
|
89
|
+
"GET",
|
|
90
|
+
"/v2.1/zones/" + zone_id + "/recordsets",
|
|
91
|
+
limit=500,
|
|
92
|
+
name=domain,
|
|
93
|
+
type=record_type,
|
|
94
|
+
line_id=line,
|
|
95
|
+
search_mode="equal",
|
|
96
|
+
)
|
|
97
|
+
records = data.get("recordsets", [])
|
|
98
|
+
record = next((r for r in records if r.get("name") == domain and r.get("type") == record_type), None)
|
|
99
|
+
return record
|
|
100
|
+
|
|
101
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
102
|
+
"""
|
|
103
|
+
v2.1 https://support.huaweicloud.com/api-dns/dns_api_64001.html
|
|
104
|
+
v2 https://support.huaweicloud.com/api-dns/CreateRecordSet.html
|
|
105
|
+
"""
|
|
106
|
+
domain = join_domain(subdomain, main_domain) + "."
|
|
107
|
+
extra["description"] = extra.get("description", self.remark)
|
|
108
|
+
res = self._request(
|
|
109
|
+
"POST",
|
|
110
|
+
"/v2.1/zones/" + zone_id + "/recordsets",
|
|
111
|
+
name=domain,
|
|
112
|
+
type=record_type,
|
|
113
|
+
records=[value],
|
|
114
|
+
ttl=ttl,
|
|
115
|
+
line=line,
|
|
116
|
+
**extra
|
|
117
|
+
)
|
|
118
|
+
if res and res.get("id"):
|
|
119
|
+
self.logger.info("Record created: %s", res)
|
|
120
|
+
return True
|
|
121
|
+
self.logger.warning("Failed to create record: %s", res)
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
125
|
+
"""https://support.huaweicloud.com/api-dns/UpdateRecordSets.html"""
|
|
126
|
+
extra["description"] = extra.get("description", self.remark)
|
|
127
|
+
# Note: The v2.1 update API does not support the line parameter in the request body
|
|
128
|
+
# The line parameter is returned in the response but cannot be modified
|
|
129
|
+
res = self._request(
|
|
130
|
+
"PUT",
|
|
131
|
+
"/v2.1/zones/" + zone_id + "/recordsets/" + old_record["id"],
|
|
132
|
+
name=old_record["name"],
|
|
133
|
+
type=record_type,
|
|
134
|
+
records=[value],
|
|
135
|
+
ttl=ttl if ttl is not None else old_record.get("ttl"),
|
|
136
|
+
**extra
|
|
137
|
+
)
|
|
138
|
+
if res and res.get("id"):
|
|
139
|
+
self.logger.info("Record updated: %s", res)
|
|
140
|
+
return True
|
|
141
|
+
self.logger.warning("Failed to update record: %s", res)
|
|
142
|
+
return False
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
Tencent Cloud DNSPod API
|
|
4
|
+
腾讯云 DNSPod API
|
|
5
|
+
|
|
6
|
+
@author: NewFuture
|
|
7
|
+
"""
|
|
8
|
+
from ._base import BaseProvider, TYPE_JSON, hmac_sha256_authorization, sha256_hash, hmac_sha256
|
|
9
|
+
from time import time, strftime, gmtime
|
|
10
|
+
from json import dumps as jsonencode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TencentCloudProvider(BaseProvider):
|
|
14
|
+
"""
|
|
15
|
+
腾讯云 DNSPod API 提供商
|
|
16
|
+
Tencent Cloud DNSPod API Provider
|
|
17
|
+
|
|
18
|
+
API Version: 2021-03-23
|
|
19
|
+
Documentation: https://cloud.tencent.com/document/api/1427
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
API = "https://dnspod.tencentcloudapi.com"
|
|
23
|
+
content_type = TYPE_JSON
|
|
24
|
+
|
|
25
|
+
# 腾讯云 DNSPod API 配置
|
|
26
|
+
service = "dnspod"
|
|
27
|
+
version_date = "2021-03-23"
|
|
28
|
+
|
|
29
|
+
def _request(self, action, **params):
|
|
30
|
+
# type: (str, **(str | int | bytes | bool | None)) -> dict | None
|
|
31
|
+
"""
|
|
32
|
+
发送腾讯云 API 请求
|
|
33
|
+
|
|
34
|
+
API 文档: https://cloud.tencent.com/document/api/1427/56187
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
action (str): API 操作名称
|
|
38
|
+
params (dict): 请求参数
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
dict: API 响应结果
|
|
42
|
+
"""
|
|
43
|
+
# 构建请求体
|
|
44
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
45
|
+
body = jsonencode(params)
|
|
46
|
+
|
|
47
|
+
# 构建请求头,小写 腾讯云只签名特定头部
|
|
48
|
+
headers = {
|
|
49
|
+
"content-type": self.content_type,
|
|
50
|
+
"host": self.API.split("://", 1)[1].strip("/"),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# 腾讯云特殊的密钥派生过程
|
|
54
|
+
date = strftime("%Y-%m-%d", gmtime())
|
|
55
|
+
credential_scope = "{}/{}/tc3_request".format(date, self.service)
|
|
56
|
+
|
|
57
|
+
# 派生签名密钥
|
|
58
|
+
secret_date = hmac_sha256("TC3" + self.auth_token, date).digest()
|
|
59
|
+
secret_service = hmac_sha256(secret_date, self.service).digest()
|
|
60
|
+
signing_key = hmac_sha256(secret_service, "tc3_request").digest()
|
|
61
|
+
|
|
62
|
+
# 预处理模板字符串
|
|
63
|
+
auth_format = "TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
|
|
64
|
+
self.auth_id,
|
|
65
|
+
credential_scope,
|
|
66
|
+
)
|
|
67
|
+
timestamp = str(int(time()))
|
|
68
|
+
sign_template = "\n".join(["TC3-HMAC-SHA256", timestamp, credential_scope, "{HashedCanonicalRequest}"])
|
|
69
|
+
authorization = hmac_sha256_authorization(
|
|
70
|
+
secret_key=signing_key,
|
|
71
|
+
method="POST",
|
|
72
|
+
path="/",
|
|
73
|
+
query="",
|
|
74
|
+
headers=headers,
|
|
75
|
+
body_hash=sha256_hash(body),
|
|
76
|
+
signing_string_format=sign_template,
|
|
77
|
+
authorization_format=auth_format,
|
|
78
|
+
)
|
|
79
|
+
# X-TC 更新签名之后方可添加
|
|
80
|
+
headers.update(
|
|
81
|
+
{
|
|
82
|
+
"X-TC-Action": action,
|
|
83
|
+
"X-TC-Version": self.version_date,
|
|
84
|
+
"X-TC-Timestamp": timestamp,
|
|
85
|
+
"authorization": authorization,
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
response = self._http("POST", "/", body=body, headers=headers)
|
|
90
|
+
if response and "Response" in response:
|
|
91
|
+
if "Error" in response["Response"]:
|
|
92
|
+
error = response["Response"]["Error"]
|
|
93
|
+
self.logger.error(
|
|
94
|
+
"TencentCloud API error: %s - %s",
|
|
95
|
+
error.get("Code", "Unknown"),
|
|
96
|
+
error.get("Message", "Unknown error"),
|
|
97
|
+
)
|
|
98
|
+
return None
|
|
99
|
+
return response["Response"]
|
|
100
|
+
|
|
101
|
+
self.logger.warning("Unexpected response format: %s", response)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def _query_zone_id(self, domain):
|
|
105
|
+
# type: (str) -> str | None
|
|
106
|
+
"""查询域名的 zone_id (domain id) https://cloud.tencent.com/document/api/1427/56173"""
|
|
107
|
+
# 使用 DescribeDomain API 查询指定域名的信息
|
|
108
|
+
response = self._request("DescribeDomain", Domain=domain)
|
|
109
|
+
|
|
110
|
+
if not response or "DomainInfo" not in response:
|
|
111
|
+
self.logger.debug("Domain info not found or query failed for: %s", domain)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
domain_id = response.get("DomainInfo", {}).get("DomainId")
|
|
115
|
+
|
|
116
|
+
if domain_id is not None:
|
|
117
|
+
self.logger.debug("Found domain %s with ID: %s", domain, domain_id)
|
|
118
|
+
return str(domain_id)
|
|
119
|
+
|
|
120
|
+
self.logger.debug("Domain ID not found in response for: %s", domain)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
124
|
+
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
125
|
+
"""查询 DNS 记录列表 https://cloud.tencent.com/document/api/1427/56166"""
|
|
126
|
+
|
|
127
|
+
response = self._request(
|
|
128
|
+
"DescribeRecordList",
|
|
129
|
+
DomainId=int(zone_id),
|
|
130
|
+
Subdomain=subdomain,
|
|
131
|
+
Domain=main_domain,
|
|
132
|
+
RecordType=record_type,
|
|
133
|
+
RecordLine=line,
|
|
134
|
+
**extra
|
|
135
|
+
)
|
|
136
|
+
if not response or "RecordList" not in response:
|
|
137
|
+
self.logger.debug("No records found or query failed")
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
records = response["RecordList"]
|
|
141
|
+
if not records:
|
|
142
|
+
self.logger.debug("No records found for subdomain: %s", subdomain)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
# 查找匹配的记录
|
|
146
|
+
target_name = subdomain if subdomain and subdomain != "@" else "@"
|
|
147
|
+
for record in records:
|
|
148
|
+
if record.get("Name") == target_name and record.get("Type") == record_type.upper():
|
|
149
|
+
self.logger.debug("Found existing record: %s", record)
|
|
150
|
+
return record
|
|
151
|
+
|
|
152
|
+
self.logger.debug("No matching record found")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
156
|
+
"""创建 DNS 记录 https://cloud.tencent.com/document/api/1427/56180"""
|
|
157
|
+
extra["Remark"] = extra.get("Remark", self.remark)
|
|
158
|
+
response = self._request(
|
|
159
|
+
"CreateRecord",
|
|
160
|
+
Domain=main_domain,
|
|
161
|
+
DomainId=int(zone_id),
|
|
162
|
+
SubDomain=subdomain,
|
|
163
|
+
RecordType=record_type,
|
|
164
|
+
Value=value,
|
|
165
|
+
RecordLine=line or "默认",
|
|
166
|
+
TTL=int(ttl) if ttl else None,
|
|
167
|
+
**extra
|
|
168
|
+
)
|
|
169
|
+
if response and "RecordId" in response:
|
|
170
|
+
self.logger.info("Record created successfully with ID: %s", response["RecordId"])
|
|
171
|
+
return True
|
|
172
|
+
self.logger.error("Failed to create record:\n%s", response)
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
176
|
+
"""更新 DNS 记录: https://cloud.tencent.com/document/api/1427/56157"""
|
|
177
|
+
extra["Remark"] = extra.get("Remark", self.remark)
|
|
178
|
+
response = self._request(
|
|
179
|
+
"ModifyRecord",
|
|
180
|
+
Domain=old_record.get("Domain", ""),
|
|
181
|
+
DomainId=old_record.get("DomainId", int(zone_id)),
|
|
182
|
+
SubDomain=old_record.get("Name"),
|
|
183
|
+
RecordId=old_record.get("RecordId"),
|
|
184
|
+
RecordType=record_type,
|
|
185
|
+
RecordLine=old_record.get("Line", line or "默认"),
|
|
186
|
+
Value=value,
|
|
187
|
+
TTL=int(ttl) if ttl else None,
|
|
188
|
+
**extra
|
|
189
|
+
)
|
|
190
|
+
if response and "RecordId" in response:
|
|
191
|
+
self.logger.info("Record updated successfully")
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
self.logger.error("Failed to update record")
|
|
195
|
+
return False
|