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.

@@ -2,264 +2,141 @@
2
2
  """
3
3
  HuaweiDNS API
4
4
  华为DNS解析操作库
5
- https://support.huaweicloud.com/api-dns/zh-cn_topic_0037134406.html
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
- from hashlib import sha256
11
- from hmac import new as hmac
12
- from binascii import hexlify
13
- from json import loads as jsondecode, dumps as jsonencode
14
- from logging import debug, info, warning
15
- from datetime import datetime
16
-
17
- try: # python 3
18
- from http.client import HTTPSConnection
19
- from urllib.parse import urlencode
20
- except ImportError: # python 2
21
- from httplib import HTTPSConnection
22
- from urllib import urlencode
23
-
24
-
25
- __author__ = 'New Future'
26
- BasicDateFormat = "%Y%m%dT%H%M%SZ"
27
- Algorithm = "SDK-HMAC-SHA256"
28
-
29
-
30
- # __all__ = ["request", "ID", "TOKEN", "PROXY"]
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
- def get_zone_id(domain):
144
- """
145
- 切割域名获取主域名和对应ID https://support.huaweicloud.com/api-dns/dns_api_62003.html
146
- 优先匹配级数最长的主域名
147
- """
148
- zoneid = None
149
- domain_slice = domain.split('.')
150
- index = len(domain_slice)
151
- root_domain = '.'.join(domain_slice[-2:])
152
- zones = request('GET', '/v2/zones', limit=500, name=root_domain)['zones']
153
- while (not zoneid) and (index >= 2):
154
- domain = '.'.join(domain_slice[-index:]) + '.'
155
- zone = next((z for z in zones if domain == (z.get('name'))), None)
156
- zoneid = zone and zone['id']
157
- index -= 1
158
- return zoneid
159
-
160
-
161
- def get_records(zoneid, **conditions):
162
- """
163
- 获取记录ID
164
- 返回满足条件的所有记录[]
165
- https://support.huaweicloud.com/api-dns/dns_api_64004.html
166
- TODO 大于500翻页
167
- """
168
- cache_key = zoneid + "_" + \
169
- conditions.get('name', "") + "_" + conditions.get('type', "")
170
- if not hasattr(get_records, 'records'):
171
- get_records.records = {} # "静态变量"存储已查询过的id
172
- get_records.keys = ('id', 'type', 'name', 'records', 'ttl')
173
-
174
- if zoneid not in get_records.records:
175
- get_records.records[cache_key] = {}
176
-
177
- data = request('GET', '/v2/zones/' + zoneid + '/recordsets',
178
- limit=500, **conditions)
179
-
180
- # https://{DNS_Endpoint}/v2/zones/2c9eb155587194ec01587224c9f90149/recordsets?limit=&offset=
181
- if data:
182
- for record in data['recordsets']:
183
- info(record)
184
- get_records.records[cache_key][record['id']] = {
185
- k: v for (k, v) in record.items() if k in get_records.keys}
186
- records = {}
187
- for (zid, record) in get_records.records[cache_key].items():
188
- for (k, value) in conditions.items():
189
- if record.get(k) != value:
190
- break
191
- else: # for else push
192
- records[zid] = record
193
- return records
194
-
195
-
196
- def update_record(domain, value, record_type='A'):
197
- """
198
- 更新记录
199
- update
200
- https://support.huaweicloud.com/api-dns/UpdateRecordSet.html
201
- add
202
- https://support.huaweicloud.com/api-dns/dns_api_64001.html
203
- """
204
- info(">>>>>%s(%s)", domain, record_type)
205
- zoneid = get_zone_id(domain)
206
- if not zoneid:
207
- raise Exception("invalid domain: [ %s ] " % domain)
208
- domain += '.'
209
- records = get_records(zoneid, name=domain, type=record_type)
210
- cache_key = zoneid + "_" + domain + "_" + record_type
211
- result = {}
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