ddns 4.0.1__py2.py3-none-any.whl → 4.1.0__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/provider/he.py CHANGED
@@ -1,83 +1,46 @@
1
1
  # coding=utf-8
2
2
  """
3
3
  Hurricane Electric (he.net) API
4
- Hurricane Electric (he.net) 接口解析操作库
5
- https://dns.he.net/docs.html
6
- @author: NN708
4
+ @author: NN708, NewFuture
7
5
  """
8
6
 
9
- from logging import debug, info, warning
10
-
11
- try: # python 3
12
- from http.client import HTTPSConnection
13
- from urllib.parse import urlencode
14
- except ImportError: # python 2
15
- from httplib import HTTPSConnection
16
- from urllib import urlencode
17
-
18
- __author__ = 'NN708'
19
-
20
-
21
- class Config:
22
- TOKEN = "password"
23
- PROXY = None # 代理设置
24
-
25
-
26
- class API:
27
- # API 配置
28
- SITE = "dyn.dns.he.net"
29
- METHOD = "POST"
30
- ACTION = "nic/update"
31
- TOKEN_PARAM = "password" # key name of token param
32
-
33
-
34
- def request(param=None, **params):
35
- """
36
- 发送请求数据
37
- """
38
- if param:
39
- params.update(param)
40
-
41
- params.update({API.TOKEN_PARAM: '***'})
42
- info("%s/%s : %s", API.SITE, API.ACTION, params)
43
- params[API.TOKEN_PARAM] = Config.TOKEN
44
-
45
- if Config.PROXY:
46
- conn = HTTPSConnection(Config.PROXY)
47
- conn.set_tunnel(API.SITE, 443)
48
- else:
49
- conn = HTTPSConnection(API.SITE)
50
-
51
- conn.request(API.METHOD, '/' + API.ACTION, urlencode(params), {
52
- "Content-type": "application/x-www-form-urlencoded"
53
- })
54
- response = conn.getresponse()
55
- res = response.read().decode('utf8')
56
- conn.close()
57
-
58
- if response.status < 200 or response.status >= 300:
59
- warning('%s : error[%d]:%s', API.ACTION, response.status, res)
60
- raise Exception(res)
61
- else:
62
- debug('%s : result:%s', API.ACTION, res)
63
- if not res:
64
- raise Exception("empty response")
65
- elif res[:5] == "nochg" or res[:4] == "good": # No change or success
66
- return res
67
- else:
68
- raise Exception(res)
69
-
70
-
71
- def update_record(domain, value, record_type="A"):
72
- """
73
- 更新记录
74
- """
75
- info(">>>>>%s(%s)", domain, record_type)
76
- res = request(hostname=domain, myip=value)
77
- if res[:4] == "good":
78
- result = "Record updated. New IP is: " + res[5:-1]
79
- elif res[:5] == "nochg":
80
- result = "IP not changed. IP is: " + res[6:-1]
81
- else:
82
- result = "Record update failed."
83
- return result
7
+ from ._base import SimpleProvider, TYPE_FORM
8
+
9
+
10
+ class HeProvider(SimpleProvider):
11
+ endpoint = "https://dyn.dns.he.net"
12
+ content_type = TYPE_FORM
13
+ accept = None # he.net does not require a specific Accept header
14
+ decode_response = False # he.net response is plain text, not JSON
15
+
16
+ def _validate(self):
17
+ self.logger.warning(
18
+ "HE.net 缺少充分的真实环境测试,请及时在 GitHub Issues 中反馈: %s",
19
+ "https://github.com/NewFuture/DDNS/issues",
20
+ )
21
+ if self.id:
22
+ raise ValueError("Hurricane Electric (he.net) does not use `id`, use `token(password)` only.")
23
+ if not self.token:
24
+ raise ValueError("Hurricane Electric (he.net) requires `token(password)`.")
25
+
26
+ def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
27
+ """
28
+ 使用 POST API 更新或创建 DNS 记录。Update or create DNS record with POST API.
29
+ https://dns.he.net/docs.html
30
+ """
31
+ self.logger.info("%s => %s(%s)", domain, value, record_type)
32
+ params = {
33
+ "hostname": domain, # he.net requires full domain name
34
+ "myip": value, # IP address to update
35
+ "password": self.token, # Use token as password
36
+ }
37
+ try:
38
+ res = self._http("POST", "/nic/update", body=params)
39
+ if res and res[:5] == "nochg" or res[:4] == "good": # No change or success
40
+ self.logger.info("HE API response: %s", res)
41
+ return True
42
+ else:
43
+ self.logger.error("HE API error: %s", res)
44
+ except Exception as e:
45
+ self.logger.error("Error updating record for %s: %s", domain, e)
46
+ return False
@@ -2,264 +2,142 @@
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 time import gmtime, strftime
9
+
10
+ from ._base import TYPE_JSON, BaseProvider, encode_params, join_domain
11
+ from ._signature import hmac_sha256_authorization, sha256_hash
12
+
13
+
14
+ class HuaweiDNSProvider(BaseProvider):
15
+ endpoint = "https://dns.myhuaweicloud.com"
16
+ content_type = TYPE_JSON
17
+ algorithm = "SDK-HMAC-SHA256"
18
+
19
+ def _validate(self):
20
+ self.logger.warning(
21
+ "华为云 DNS 缺少充分的真实环境测试,请及时在 GitHub Issues 中反馈: %s",
22
+ "https://github.com/NewFuture/DDNS/issues",
23
+ )
24
+ super(HuaweiDNSProvider, self)._validate()
25
+
26
+ def _request(self, method, path, **params):
27
+ """
28
+ https://support.huaweicloud.com/api-dns/zh-cn_topic_0037134406.html
29
+ https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-002.html
30
+ https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-003.html
31
+ https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-004.html
32
+ """
33
+ # type: (str, str, **Any) -> dict
34
+ params = {k: v for k, v in params.items() if v is not None}
35
+ if method.upper() == "GET" or method.upper() == "DELETE":
36
+ query = encode_params(params)
37
+ body = ""
38
+ else:
39
+ query = ""
40
+ body = self._encode_body(params)
41
+
42
+ now = strftime("%Y%m%dT%H%M%SZ", gmtime())
43
+ headers = {
44
+ "content-type": self.content_type,
45
+ "host": self.endpoint.split("://", 1)[1].strip("/"),
46
+ "X-Sdk-Date": now,
47
+ }
9
48
 
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)
49
+ # 使用通用签名函数
50
+ body_hash = sha256_hash(body)
51
+ # 华为云需要在签名时使用带尾斜杠的路径
52
+ sign_path = path if path.endswith("/") else path + "/"
53
+ authorization_format = "%s Access=%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
54
+ self.algorithm,
55
+ self.id,
56
+ )
57
+ authorization = hmac_sha256_authorization(
58
+ secret_key=self.token,
59
+ method=method,
60
+ path=sign_path,
61
+ query=query,
62
+ headers=headers,
63
+ body_hash=body_hash,
64
+ signing_string_format=self.algorithm + "\n" + now + "\n{HashedCanonicalRequest}",
65
+ authorization_format=authorization_format,
66
+ )
67
+ headers["Authorization"] = authorization
68
+
69
+ # 使用原始路径发送实际请求
70
+ path = "{}?{}".format(path, query) if query else path
71
+ data = self._http(method, path, headers=headers, body=body)
140
72
  return data
141
73
 
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
74
+ def _query_zone_id(self, domain):
75
+ """https://support.huaweicloud.com/api-dns/dns_api_62003.html"""
76
+ domain = domain + "." if not domain.endswith(".") else domain
77
+ data = self._request("GET", "/v2/zones", search_mode="equal", limit=500, name=domain)
78
+ zones = data.get("zones", [])
79
+ zone = next((z for z in zones if domain == z.get("name")), None)
80
+ zoneid = zone and zone["id"]
81
+ return zoneid
82
+
83
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
84
+ """
85
+ v2.1 https://support.huaweicloud.com/api-dns/dns_api_64004.html
86
+ v2 https://support.huaweicloud.com/api-dns/ListRecordSetsByZone.html
87
+ """
88
+ domain = join_domain(subdomain, main_domain) + "."
89
+ data = self._request(
90
+ "GET",
91
+ "/v2.1/zones/" + zone_id + "/recordsets",
92
+ limit=500,
93
+ name=domain,
94
+ type=record_type,
95
+ line_id=line,
96
+ search_mode="equal",
97
+ )
98
+ records = data.get("recordsets", [])
99
+ record = next((r for r in records if r.get("name") == domain and r.get("type") == record_type), None)
100
+ return record
101
+
102
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
103
+ """
104
+ v2.1 https://support.huaweicloud.com/api-dns/dns_api_64001.html
105
+ v2 https://support.huaweicloud.com/api-dns/CreateRecordSet.html
106
+ """
107
+ domain = join_domain(subdomain, main_domain) + "."
108
+ extra["description"] = extra.get("description", self.remark)
109
+ res = self._request(
110
+ "POST",
111
+ "/v2.1/zones/" + zone_id + "/recordsets",
112
+ name=domain,
113
+ type=record_type,
114
+ records=[value],
115
+ ttl=ttl,
116
+ line=line,
117
+ **extra
118
+ ) # fmt: skip
119
+ if res and res.get("id"):
120
+ self.logger.info("Record created: %s", res)
121
+ return True
122
+ self.logger.warning("Failed to create record: %s", res)
123
+ return False
124
+
125
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
126
+ """https://support.huaweicloud.com/api-dns/UpdateRecordSets.html"""
127
+ extra["description"] = extra.get("description", self.remark)
128
+ # Note: The v2.1 update API does not support the line parameter in the request body
129
+ # The line parameter is returned in the response but cannot be modified
130
+ res = self._request(
131
+ "PUT",
132
+ "/v2.1/zones/" + zone_id + "/recordsets/" + old_record["id"],
133
+ name=old_record["name"],
134
+ type=record_type,
135
+ records=[value],
136
+ ttl=ttl if ttl is not None else old_record.get("ttl"),
137
+ **extra
138
+ ) # fmt: skip
139
+ if res and res.get("id"):
140
+ self.logger.info("Record updated: %s", res)
141
+ return True
142
+ self.logger.warning("Failed to update record: %s", res)
143
+ return False