ddns 4.1.0b1__py2.py3-none-any.whl → 4.1.0b3__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 +1 -0
- ddns/__init__.py +2 -11
- ddns/__main__.py +91 -138
- ddns/{util/cache.py → cache.py} +42 -11
- ddns/{util/ip.py → ip.py} +11 -11
- ddns/provider/__init__.py +24 -4
- ddns/provider/_base.py +65 -198
- ddns/provider/_signature.py +113 -0
- ddns/provider/alidns.py +35 -17
- ddns/provider/aliesa.py +130 -0
- ddns/provider/callback.py +10 -10
- ddns/provider/cloudflare.py +10 -10
- ddns/provider/debug.py +0 -1
- ddns/provider/dnscom.py +7 -6
- ddns/provider/dnspod.py +4 -7
- ddns/provider/dnspod_com.py +1 -1
- ddns/provider/edgeone.py +83 -0
- ddns/provider/he.py +4 -4
- ddns/provider/huaweidns.py +12 -11
- ddns/provider/namesilo.py +159 -0
- ddns/provider/noip.py +101 -0
- ddns/provider/tencentcloud.py +13 -14
- ddns/util/comment.py +88 -0
- ddns/util/fileio.py +113 -0
- ddns/util/http.py +275 -230
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b3.dist-info}/METADATA +95 -119
- ddns-4.1.0b3.dist-info/RECORD +32 -0
- ddns/util/config.py +0 -314
- ddns-4.1.0b1.dist-info/RECORD +0 -26
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b3.dist-info}/WHEEL +0 -0
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b3.dist-info}/entry_points.txt +0 -0
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b3.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b3.dist-info}/top_level.txt +0 -0
ddns/provider/_base.py
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
# coding=utf-8
|
|
2
2
|
"""
|
|
3
3
|
## SimpleProvider 简单DNS抽象基类
|
|
4
|
-
|
|
5
4
|
* set_record()
|
|
6
5
|
|
|
7
6
|
## BaseProvider 标准DNS抽象基类
|
|
8
7
|
定义所有 DNS 服务商 API 类应继承的抽象基类,统一接口,便于扩展适配多服务商。
|
|
9
|
-
|
|
10
|
-
Abstract base class for DNS provider APIs.
|
|
11
8
|
Defines a unified interface to support extension and adaptation across providers.
|
|
12
9
|
* _query_zone_id
|
|
13
10
|
* _query_record
|
|
14
11
|
* _update_record
|
|
15
12
|
* _create_record
|
|
13
|
+
|
|
16
14
|
┌──────────────────────────────────────────────────┐
|
|
17
|
-
│
|
|
15
|
+
│ 调用 set_record(domain, value...) │
|
|
18
16
|
└──────────────────────────────────────────────────┘
|
|
19
17
|
│
|
|
20
18
|
▼
|
|
@@ -58,121 +56,30 @@ Defines a unified interface to support extension and adaptation across providers
|
|
|
58
56
|
"""
|
|
59
57
|
|
|
60
58
|
from abc import ABCMeta, abstractmethod
|
|
61
|
-
from hashlib import sha256
|
|
62
|
-
from hmac import HMAC
|
|
63
59
|
from json import loads as jsondecode, dumps as jsonencode
|
|
64
60
|
from logging import Logger, getLogger # noqa:F401 # type: ignore[no-redef]
|
|
65
|
-
from
|
|
66
|
-
from ..util.http import send_http_request
|
|
67
|
-
|
|
68
|
-
try: # python 3
|
|
69
|
-
from urllib.parse import quote, urlencode
|
|
70
|
-
except ImportError: # python 2
|
|
71
|
-
from urllib import urlencode, quote # type: ignore[no-redef,import-untyped]
|
|
61
|
+
from ..util.http import request, quote, urlencode
|
|
72
62
|
|
|
73
63
|
TYPE_FORM = "application/x-www-form-urlencoded"
|
|
74
64
|
TYPE_JSON = "application/json"
|
|
75
65
|
|
|
76
66
|
|
|
77
|
-
def
|
|
78
|
-
# type: (str
|
|
67
|
+
def encode_params(params):
|
|
68
|
+
# type: (dict|list|str|bytes|None) -> str
|
|
79
69
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Compute HMAC-SHA256 signature object.
|
|
70
|
+
编码参数为 URL 查询字符串,参数顺序会排序
|
|
83
71
|
|
|
84
72
|
Args:
|
|
85
|
-
|
|
86
|
-
message (str | bytes): 待签名消息 / Message to sign
|
|
87
|
-
|
|
73
|
+
params (dict|list|str|bytes|None): 参数字典、列表或字符串
|
|
88
74
|
Returns:
|
|
89
|
-
|
|
90
|
-
HMAC signature object, call .digest() for bytes or .hexdigest() for hex string
|
|
91
|
-
"""
|
|
92
|
-
# Python 2/3 compatible encoding - avoid double encoding in Python 2
|
|
93
|
-
if not isinstance(key, bytes):
|
|
94
|
-
key = key.encode("utf-8")
|
|
95
|
-
if not isinstance(message, bytes):
|
|
96
|
-
message = message.encode("utf-8")
|
|
97
|
-
return HMAC(key, message, sha256)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def sha256_hash(data):
|
|
101
|
-
# type: (str | bytes) -> str
|
|
75
|
+
str: 编码后的查询字符串
|
|
102
76
|
"""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Returns:
|
|
111
|
-
str: 十六进制哈希字符串 / Hexadecimal hash string
|
|
112
|
-
"""
|
|
113
|
-
# Python 2/3 compatible encoding - avoid double encoding in Python 2
|
|
114
|
-
if not isinstance(data, bytes):
|
|
115
|
-
data = data.encode("utf-8")
|
|
116
|
-
return sha256(data).hexdigest()
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def hmac_sha256_authorization(
|
|
120
|
-
secret_key, # type: str | bytes
|
|
121
|
-
method, # type: str
|
|
122
|
-
path, # type: str
|
|
123
|
-
query, # type: str
|
|
124
|
-
headers, # type: dict[str, str]
|
|
125
|
-
body_hash, # type: str
|
|
126
|
-
signing_string_format, # type: str
|
|
127
|
-
authorization_format, # type: str
|
|
128
|
-
):
|
|
129
|
-
# type: (...) -> str
|
|
130
|
-
"""
|
|
131
|
-
HMAC-SHA256 云服务商通用认证签名生成器
|
|
132
|
-
|
|
133
|
-
Universal cloud provider authentication signature generator using HMAC-SHA256.
|
|
134
|
-
|
|
135
|
-
通用的云服务商API认证签名生成函数,使用HMAC-SHA256算法生成符合各云服务商规范的Authorization头部。
|
|
136
|
-
|
|
137
|
-
模板变量格式:{HashedCanonicalRequest}, {SignedHeaders}, {Signature}
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
secret_key (str | bytes): 签名密钥,已经过密钥派生处理 / Signing key (already derived if needed)
|
|
141
|
-
method (str): HTTP请求方法 / HTTP request method (GET, POST, etc.)
|
|
142
|
-
path (str): API请求路径 / API request path
|
|
143
|
-
query (str): URL查询字符串 / URL query string
|
|
144
|
-
headers (dict[str, str]): HTTP请求头部 / HTTP request headers
|
|
145
|
-
body_hash (str): 请求体的SHA256哈希值 / SHA256 hash of request payload
|
|
146
|
-
signing_string_format (str): 待签名字符串模板,包含 {HashedCanonicalRequest} 占位符
|
|
147
|
-
authorization_format (str): Authorization头部模板,包含 {SignedHeaders}, {Signature} 占位符
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
str: 完整的Authorization头部值 / Complete Authorization header value
|
|
151
|
-
"""
|
|
152
|
-
# 1. 构建规范化头部 - 所有传入的头部都参与签名
|
|
153
|
-
headers_to_sign = {k.lower(): str(v).strip() for k, v in headers.items()}
|
|
154
|
-
signed_headers_list = sorted(headers_to_sign.keys())
|
|
155
|
-
|
|
156
|
-
# 2. 构建规范请求字符串
|
|
157
|
-
canonical_headers = ""
|
|
158
|
-
for header_name in signed_headers_list:
|
|
159
|
-
canonical_headers += "{}:{}\n".format(header_name, headers_to_sign[header_name])
|
|
160
|
-
|
|
161
|
-
# 构建完整的规范请求字符串
|
|
162
|
-
signed_headers = ";".join(signed_headers_list)
|
|
163
|
-
canonical_request = "\n".join([method.upper(), path, query, canonical_headers, signed_headers, body_hash])
|
|
164
|
-
|
|
165
|
-
# 3. 构建待签名字符串 - 只需要替换 HashedCanonicalRequest
|
|
166
|
-
hashed_canonical_request = sha256_hash(canonical_request)
|
|
167
|
-
string_to_sign = signing_string_format.format(HashedCanonicalRequest=hashed_canonical_request)
|
|
168
|
-
|
|
169
|
-
# 4. 计算最终签名
|
|
170
|
-
signature = hmac_sha256(secret_key, string_to_sign).hexdigest()
|
|
171
|
-
|
|
172
|
-
# 5. 构建Authorization头部 - 只需要替换 SignedHeaders 和 Signature
|
|
173
|
-
authorization = authorization_format.format(SignedHeaders=signed_headers, Signature=signature)
|
|
174
|
-
|
|
175
|
-
return authorization
|
|
77
|
+
if not params:
|
|
78
|
+
return ""
|
|
79
|
+
elif isinstance(params, (str, bytes)):
|
|
80
|
+
return params # type: ignore[return-value]
|
|
81
|
+
items = params.items() if isinstance(params, dict) else params
|
|
82
|
+
return urlencode(sorted(items), doseq=True)
|
|
176
83
|
|
|
177
84
|
|
|
178
85
|
class SimpleProvider(object):
|
|
@@ -188,43 +95,45 @@ class SimpleProvider(object):
|
|
|
188
95
|
__metaclass__ = ABCMeta
|
|
189
96
|
|
|
190
97
|
# API endpoint domain (to be defined in subclass)
|
|
191
|
-
|
|
98
|
+
endpoint = "" # type: str # https://exampledns.com
|
|
192
99
|
# Content-Type for requests (to be defined in subclass)
|
|
193
100
|
content_type = TYPE_FORM # type: Literal["application/x-www-form-urlencoded"] | Literal["application/json"]
|
|
194
101
|
# 默认 accept 头部, 空则不设置
|
|
195
102
|
accept = TYPE_JSON # type: str | None
|
|
196
103
|
# Decode Response as JSON by default
|
|
197
|
-
decode_response = True
|
|
198
|
-
# 是否验证 SSL 证书,默认为 True
|
|
199
|
-
verify_ssl = "auto" # type: bool | str
|
|
200
|
-
|
|
201
|
-
# 版本
|
|
202
|
-
version = environ.get("DDNS_VERSION", "0.0.0")
|
|
104
|
+
decode_response = True # type: bool
|
|
203
105
|
# Description
|
|
204
|
-
remark = "Managed by [DDNS
|
|
106
|
+
remark = "Managed by [DDNS](https://ddns.newfuture.cc)"
|
|
205
107
|
|
|
206
|
-
def __init__(self,
|
|
207
|
-
# type: (str, str, Logger | None, bool|str| None, **object) -> None
|
|
108
|
+
def __init__(self, id, token, logger=None, ssl="auto", proxy=None, endpoint=None, **options):
|
|
109
|
+
# type: (str, str, Logger | None, bool|str, list[str]|None, str|None, **object) -> None
|
|
208
110
|
"""
|
|
209
111
|
初始化服务商对象
|
|
210
112
|
|
|
211
113
|
Initialize provider instance.
|
|
212
114
|
|
|
213
115
|
Args:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
116
|
+
id (str): 身份认证 ID / Authentication ID
|
|
117
|
+
token (str): 密钥 / Authentication Token
|
|
118
|
+
proxy (list[str | None] | None): 代理配置,支持代理列表
|
|
119
|
+
options (dict): 其它参数 / Additional options
|
|
217
120
|
"""
|
|
218
|
-
self.
|
|
219
|
-
self.
|
|
121
|
+
self.id = id
|
|
122
|
+
self.token = token
|
|
123
|
+
if endpoint:
|
|
124
|
+
self.endpoint = endpoint
|
|
125
|
+
|
|
126
|
+
# 处理代理配置
|
|
127
|
+
self._proxy = proxy # 代理列表或None
|
|
128
|
+
|
|
129
|
+
self._ssl = ssl
|
|
130
|
+
|
|
220
131
|
self.options = options
|
|
221
132
|
name = self.__class__.__name__
|
|
222
133
|
self.logger = (logger or getLogger()).getChild(name)
|
|
223
|
-
|
|
224
|
-
if verify_ssl is not None:
|
|
225
|
-
self.verify_ssl = verify_ssl
|
|
134
|
+
|
|
226
135
|
self._zone_map = {} # type: dict[str, str]
|
|
227
|
-
self.logger.debug("%s initialized with: %s", self.__class__.__name__,
|
|
136
|
+
self.logger.debug("%s initialized with: %s", self.__class__.__name__, id)
|
|
228
137
|
self._validate() # 验证身份认证信息
|
|
229
138
|
|
|
230
139
|
@abstractmethod
|
|
@@ -248,22 +157,6 @@ class SimpleProvider(object):
|
|
|
248
157
|
"""
|
|
249
158
|
raise NotImplementedError("This set_record should be implemented by subclasses")
|
|
250
159
|
|
|
251
|
-
def set_proxy(self, proxy_str):
|
|
252
|
-
# type: (str | None) -> SimpleProvider
|
|
253
|
-
"""
|
|
254
|
-
设置代理服务器
|
|
255
|
-
|
|
256
|
-
Set HTTPS proxy string.
|
|
257
|
-
|
|
258
|
-
Args:
|
|
259
|
-
proxy_str (str): 代理地址
|
|
260
|
-
|
|
261
|
-
Returns:
|
|
262
|
-
Self: 自身
|
|
263
|
-
"""
|
|
264
|
-
self.proxy = proxy_str
|
|
265
|
-
return self
|
|
266
|
-
|
|
267
160
|
def _validate(self):
|
|
268
161
|
# type: () -> None
|
|
269
162
|
"""
|
|
@@ -271,11 +164,11 @@ class SimpleProvider(object):
|
|
|
271
164
|
|
|
272
165
|
Validate authentication credentials.
|
|
273
166
|
"""
|
|
274
|
-
if not self.
|
|
167
|
+
if not self.id:
|
|
275
168
|
raise ValueError("id must be configured")
|
|
276
|
-
if not self.
|
|
169
|
+
if not self.token:
|
|
277
170
|
raise ValueError("token must be configured")
|
|
278
|
-
if not self.
|
|
171
|
+
if not self.endpoint:
|
|
279
172
|
raise ValueError("API endpoint must be defined in {}".format(self.__class__.__name__))
|
|
280
173
|
|
|
281
174
|
def _http(self, method, url, params=None, body=None, queries=None, headers=None): # noqa: C901
|
|
@@ -314,13 +207,13 @@ class SimpleProvider(object):
|
|
|
314
207
|
|
|
315
208
|
# 构建查询字符串
|
|
316
209
|
if len(query_params) > 0:
|
|
317
|
-
url += ("&" if "?" in url else "?") +
|
|
210
|
+
url += ("&" if "?" in url else "?") + encode_params(query_params)
|
|
318
211
|
|
|
319
212
|
# 构建完整URL
|
|
320
213
|
if not url.startswith("http://") and not url.startswith("https://"):
|
|
321
|
-
if not url.startswith("/") and self.
|
|
214
|
+
if not url.startswith("/") and self.endpoint.endswith("/"):
|
|
322
215
|
url = "/" + url
|
|
323
|
-
url = self.
|
|
216
|
+
url = self.endpoint + url
|
|
324
217
|
|
|
325
218
|
# 记录请求日志
|
|
326
219
|
self.logger.info("%s %s", method, self._mask_sensitive_data(url))
|
|
@@ -330,12 +223,7 @@ class SimpleProvider(object):
|
|
|
330
223
|
if body:
|
|
331
224
|
if "content-type" not in headers:
|
|
332
225
|
headers["content-type"] = self.content_type
|
|
333
|
-
|
|
334
|
-
body_data = body
|
|
335
|
-
elif self.content_type == TYPE_FORM:
|
|
336
|
-
body_data = self._encode(body)
|
|
337
|
-
else:
|
|
338
|
-
body_data = jsonencode(body)
|
|
226
|
+
body_data = self._encode_body(body)
|
|
339
227
|
self.logger.debug("body:\n%s", self._mask_sensitive_data(body_data))
|
|
340
228
|
|
|
341
229
|
# 处理headers
|
|
@@ -344,16 +232,8 @@ class SimpleProvider(object):
|
|
|
344
232
|
if len(headers) > 2:
|
|
345
233
|
self.logger.debug("headers:\n%s", {k: self._mask_sensitive_data(v) for k, v in headers.items()})
|
|
346
234
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
method=method,
|
|
350
|
-
body=body_data,
|
|
351
|
-
headers=headers,
|
|
352
|
-
proxy=self.proxy,
|
|
353
|
-
max_redirects=5,
|
|
354
|
-
verify_ssl=self.verify_ssl,
|
|
355
|
-
)
|
|
356
|
-
|
|
235
|
+
# 直接传递代理列表给request函数
|
|
236
|
+
response = request(method, url, body_data, headers=headers, proxies=self._proxy, verify=self._ssl, retries=2)
|
|
357
237
|
# 处理响应
|
|
358
238
|
status_code = response.status
|
|
359
239
|
if not (200 <= status_code < 300):
|
|
@@ -364,11 +244,11 @@ class SimpleProvider(object):
|
|
|
364
244
|
if status_code >= 500 or status_code in (400, 401, 403):
|
|
365
245
|
self.logger.error("HTTP error:\n%s", res)
|
|
366
246
|
if status_code == 400:
|
|
367
|
-
raise RuntimeError("
|
|
247
|
+
raise RuntimeError("参数错误 [400]: " + response.reason)
|
|
368
248
|
elif status_code == 401:
|
|
369
249
|
raise RuntimeError("认证失败 [401]: " + response.reason)
|
|
370
250
|
elif status_code == 403:
|
|
371
|
-
raise RuntimeError("
|
|
251
|
+
raise RuntimeError("禁止访问 [403]: " + response.reason)
|
|
372
252
|
else:
|
|
373
253
|
raise RuntimeError("服务器错误 [{}]: {}".format(status_code, response.reason))
|
|
374
254
|
|
|
@@ -382,36 +262,23 @@ class SimpleProvider(object):
|
|
|
382
262
|
self.logger.error("fail to decode response: %s", e)
|
|
383
263
|
return res
|
|
384
264
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
# type: (dict|list|str|bytes|None) -> str
|
|
265
|
+
def _encode_body(self, data):
|
|
266
|
+
# type: (dict | list | str | bytes | None) -> str
|
|
388
267
|
"""
|
|
389
|
-
|
|
390
|
-
|
|
268
|
+
自动编码数据为字符串或字节, 根据 content_type 选择编码方式。
|
|
391
269
|
Args:
|
|
392
|
-
|
|
393
|
-
Returns:
|
|
394
|
-
str: 编码后的查询字符串
|
|
395
|
-
"""
|
|
396
|
-
if not params:
|
|
397
|
-
return ""
|
|
398
|
-
elif isinstance(params, (str, bytes)):
|
|
399
|
-
return params # type: ignore[return-value]
|
|
400
|
-
return urlencode(params, doseq=True)
|
|
401
|
-
|
|
402
|
-
@staticmethod
|
|
403
|
-
def _quote(data, safe="/"):
|
|
404
|
-
# type: (str, str) -> str
|
|
405
|
-
"""
|
|
406
|
-
对字符串进行 URL 编码
|
|
407
|
-
|
|
408
|
-
Args:
|
|
409
|
-
data (str): 待编码字符串
|
|
270
|
+
data (dict | list | str | bytes | None): 待编码数据
|
|
410
271
|
|
|
411
272
|
Returns:
|
|
412
|
-
str:
|
|
273
|
+
str | bytes | None: 编码后的数据
|
|
413
274
|
"""
|
|
414
|
-
|
|
275
|
+
if isinstance(data, (str, bytes)):
|
|
276
|
+
return data # type: ignore[return-value]
|
|
277
|
+
if not data:
|
|
278
|
+
return ""
|
|
279
|
+
if self.content_type == TYPE_FORM:
|
|
280
|
+
return encode_params(data)
|
|
281
|
+
return jsonencode(data)
|
|
415
282
|
|
|
416
283
|
def _mask_sensitive_data(self, data):
|
|
417
284
|
# type: (str | bytes | None) -> str | bytes | None
|
|
@@ -423,19 +290,19 @@ class SimpleProvider(object):
|
|
|
423
290
|
Returns:
|
|
424
291
|
str | bytes | None: 打码后的字符串
|
|
425
292
|
"""
|
|
426
|
-
if not data or not self.
|
|
293
|
+
if not data or not self.token:
|
|
427
294
|
return data
|
|
428
295
|
|
|
429
296
|
# 生成打码后的token
|
|
430
|
-
token_masked = self.
|
|
431
|
-
token_encoded = quote(self.
|
|
297
|
+
token_masked = self.token[:2] + "***" + self.token[-2:] if len(self.token) > 4 else "***"
|
|
298
|
+
token_encoded = quote(self.token, safe="")
|
|
432
299
|
|
|
433
300
|
if isinstance(data, bytes): # 处理字节数据
|
|
434
|
-
return data.replace(self.
|
|
301
|
+
return data.replace(self.token.encode(), token_masked.encode()).replace(
|
|
435
302
|
token_encoded.encode(), token_masked.encode()
|
|
436
303
|
)
|
|
437
304
|
if hasattr(data, "replace"): # 处理字符串数据
|
|
438
|
-
return data.replace(self.
|
|
305
|
+
return data.replace(self.token, token_masked).replace(token_encoded, token_masked)
|
|
439
306
|
return data
|
|
440
307
|
|
|
441
308
|
|
|
@@ -472,7 +339,7 @@ class BaseProvider(SimpleProvider):
|
|
|
472
339
|
"""
|
|
473
340
|
domain = domain.lower()
|
|
474
341
|
self.logger.info("%s => %s(%s)", domain, value, record_type)
|
|
475
|
-
sub, main =
|
|
342
|
+
sub, main = _split_custom_domain(domain)
|
|
476
343
|
try:
|
|
477
344
|
if sub is not None:
|
|
478
345
|
# 使用自定义分隔符格式
|
|
@@ -619,7 +486,7 @@ class BaseProvider(SimpleProvider):
|
|
|
619
486
|
return None, None, main
|
|
620
487
|
|
|
621
488
|
|
|
622
|
-
def
|
|
489
|
+
def _split_custom_domain(domain):
|
|
623
490
|
# type: (str) -> tuple[str | None, str]
|
|
624
491
|
"""
|
|
625
492
|
拆分支持 ~ 或 + 的自定义格式域名为 (子域, 主域)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
DNS Provider 签名和哈希算法模块
|
|
4
|
+
|
|
5
|
+
Signature and hash algorithms module for DNS providers.
|
|
6
|
+
Provides common cryptographic functions for cloud provider authentication.
|
|
7
|
+
|
|
8
|
+
@author: NewFuture
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from hashlib import sha256
|
|
12
|
+
from hmac import HMAC
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def hmac_sha256(key, message):
|
|
16
|
+
# type: (str | bytes, str | bytes) -> HMAC
|
|
17
|
+
"""
|
|
18
|
+
计算 HMAC-SHA256 签名对象
|
|
19
|
+
|
|
20
|
+
Compute HMAC-SHA256 signature object.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
key (str | bytes): 签名密钥 / Signing key
|
|
24
|
+
message (str | bytes): 待签名消息 / Message to sign
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
HMAC: HMAC签名对象,可调用.digest()获取字节或.hexdigest()获取十六进制字符串
|
|
28
|
+
HMAC signature object, call .digest() for bytes or .hexdigest() for hex string
|
|
29
|
+
"""
|
|
30
|
+
# Python 2/3 compatible encoding - avoid double encoding in Python 2
|
|
31
|
+
if not isinstance(key, bytes):
|
|
32
|
+
key = key.encode("utf-8")
|
|
33
|
+
if not isinstance(message, bytes):
|
|
34
|
+
message = message.encode("utf-8")
|
|
35
|
+
return HMAC(key, message, sha256)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sha256_hash(data):
|
|
39
|
+
# type: (str | bytes) -> str
|
|
40
|
+
"""
|
|
41
|
+
计算 SHA256 哈希值
|
|
42
|
+
|
|
43
|
+
Compute SHA256 hash.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
data (str | bytes): 待哈希数据 / Data to hash
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: 十六进制哈希字符串 / Hexadecimal hash string
|
|
50
|
+
"""
|
|
51
|
+
# Python 2/3 compatible encoding - avoid double encoding in Python 2
|
|
52
|
+
if not isinstance(data, bytes):
|
|
53
|
+
data = data.encode("utf-8")
|
|
54
|
+
return sha256(data).hexdigest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def hmac_sha256_authorization(
|
|
58
|
+
secret_key, # type: str | bytes
|
|
59
|
+
method, # type: str
|
|
60
|
+
path, # type: str
|
|
61
|
+
query, # type: str
|
|
62
|
+
headers, # type: dict[str, str]
|
|
63
|
+
body_hash, # type: str
|
|
64
|
+
signing_string_format, # type: str
|
|
65
|
+
authorization_format, # type: str
|
|
66
|
+
):
|
|
67
|
+
# type: (...) -> str
|
|
68
|
+
"""
|
|
69
|
+
HMAC-SHA256 云服务商通用认证签名生成器
|
|
70
|
+
|
|
71
|
+
Universal cloud provider authentication signature generator using HMAC-SHA256.
|
|
72
|
+
|
|
73
|
+
通用的云服务商API认证签名生成函数,使用HMAC-SHA256算法生成符合各云服务商规范的Authorization头部。
|
|
74
|
+
|
|
75
|
+
模板变量格式:{HashedCanonicalRequest}, {SignedHeaders}, {Signature}
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
secret_key (str | bytes): 签名密钥,已经过密钥派生处理 / Signing key (already derived if needed)
|
|
79
|
+
method (str): HTTP请求方法 / HTTP request method (GET, POST, etc.)
|
|
80
|
+
path (str): API请求路径 / API request path
|
|
81
|
+
query (str): URL查询字符串 / URL query string
|
|
82
|
+
headers (dict[str, str]): HTTP请求头部 / HTTP request headers
|
|
83
|
+
body_hash (str): 请求体的SHA256哈希值 / SHA256 hash of request payload
|
|
84
|
+
signing_string_format (str): 待签名字符串模板,包含 {HashedCanonicalRequest} 占位符
|
|
85
|
+
authorization_format (str): Authorization头部模板,包含 {SignedHeaders}, {Signature} 占位符
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
str: 完整的Authorization头部值 / Complete Authorization header value
|
|
89
|
+
"""
|
|
90
|
+
# 1. 构建规范化头部 - 所有传入的头部都参与签名
|
|
91
|
+
headers_to_sign = {k.lower(): str(v).strip() for k, v in headers.items()}
|
|
92
|
+
signed_headers_list = sorted(headers_to_sign.keys())
|
|
93
|
+
|
|
94
|
+
# 2. 构建规范请求字符串
|
|
95
|
+
canonical_headers = ""
|
|
96
|
+
for header_name in signed_headers_list:
|
|
97
|
+
canonical_headers += "{}:{}\n".format(header_name, headers_to_sign[header_name])
|
|
98
|
+
|
|
99
|
+
# 构建完整的规范请求字符串
|
|
100
|
+
signed_headers = ";".join(signed_headers_list)
|
|
101
|
+
canonical_request = "\n".join([method.upper(), path, query, canonical_headers, signed_headers, body_hash])
|
|
102
|
+
|
|
103
|
+
# 3. 构建待签名字符串 - 只需要替换 HashedCanonicalRequest
|
|
104
|
+
hashed_canonical_request = sha256_hash(canonical_request)
|
|
105
|
+
string_to_sign = signing_string_format.format(HashedCanonicalRequest=hashed_canonical_request)
|
|
106
|
+
|
|
107
|
+
# 4. 计算最终签名
|
|
108
|
+
signature = hmac_sha256(secret_key, string_to_sign).hexdigest()
|
|
109
|
+
|
|
110
|
+
# 5. 构建Authorization头部 - 只需要替换 SignedHeaders 和 Signature
|
|
111
|
+
authorization = authorization_format.format(SignedHeaders=signed_headers, Signature=signature)
|
|
112
|
+
|
|
113
|
+
return authorization
|
ddns/provider/alidns.py
CHANGED
|
@@ -5,25 +5,38 @@ AliDNS API
|
|
|
5
5
|
@author: NewFuture
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
from time import strftime, gmtime, time
|
|
8
|
+
from time import gmtime, strftime, time
|
|
10
9
|
|
|
10
|
+
from ._base import TYPE_FORM, BaseProvider, encode_params, join_domain
|
|
11
|
+
from ._signature import hmac_sha256_authorization, sha256_hash
|
|
11
12
|
|
|
12
|
-
class AlidnsProvider(BaseProvider):
|
|
13
|
-
API = "https://alidns.aliyuncs.com"
|
|
14
|
-
content_type = TYPE_FORM # 阿里云DNS API使用表单格式
|
|
15
13
|
|
|
14
|
+
class AliBaseProvider(BaseProvider):
|
|
15
|
+
"""阿里云基础Provider,提供通用的_request方法"""
|
|
16
|
+
|
|
17
|
+
endpoint = "https://alidns.aliyuncs.com"
|
|
18
|
+
content_type = TYPE_FORM # 阿里云DNS API使用表单格式
|
|
16
19
|
api_version = "2015-01-09" # API版本,v3签名需要
|
|
17
20
|
|
|
18
|
-
def _request(self, action, **params):
|
|
19
|
-
# type: (str, **(
|
|
21
|
+
def _request(self, action, method="POST", **params):
|
|
22
|
+
# type: (str, str, **(Any)) -> dict
|
|
20
23
|
"""Aliyun v3 https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature"""
|
|
21
24
|
params = {k: v for k, v in params.items() if v is not None}
|
|
22
|
-
|
|
25
|
+
|
|
26
|
+
if method in ("GET", "DELETE"):
|
|
27
|
+
# For GET and DELETE requests, parameters go in query string
|
|
28
|
+
query_string = encode_params(params) if len(params) > 0 else ""
|
|
29
|
+
body_content = ""
|
|
30
|
+
else:
|
|
31
|
+
# For POST requests, parameters go in body
|
|
32
|
+
body_content = self._encode_body(params)
|
|
33
|
+
query_string = ""
|
|
34
|
+
|
|
35
|
+
path = "/"
|
|
23
36
|
content_hash = sha256_hash(body_content)
|
|
24
37
|
# 构造请求头部
|
|
25
38
|
headers = {
|
|
26
|
-
"host": self.
|
|
39
|
+
"host": self.endpoint.split("://", 1)[1].strip("/"),
|
|
27
40
|
"content-type": self.content_type,
|
|
28
41
|
"x-acs-action": action,
|
|
29
42
|
"x-acs-content-sha256": content_hash,
|
|
@@ -34,20 +47,25 @@ class AlidnsProvider(BaseProvider):
|
|
|
34
47
|
|
|
35
48
|
# 使用通用签名函数
|
|
36
49
|
authorization = hmac_sha256_authorization(
|
|
37
|
-
secret_key=self.
|
|
38
|
-
method=
|
|
39
|
-
path=
|
|
40
|
-
query=
|
|
50
|
+
secret_key=self.token,
|
|
51
|
+
method=method,
|
|
52
|
+
path=path,
|
|
53
|
+
query=query_string,
|
|
41
54
|
headers=headers,
|
|
42
55
|
body_hash=content_hash,
|
|
43
56
|
signing_string_format="ACS3-HMAC-SHA256\n{HashedCanonicalRequest}",
|
|
44
57
|
authorization_format=(
|
|
45
|
-
"ACS3-HMAC-SHA256 Credential=" + self.
|
|
58
|
+
"ACS3-HMAC-SHA256 Credential=" + self.id + ",SignedHeaders={SignedHeaders},Signature={Signature}"
|
|
46
59
|
),
|
|
47
60
|
)
|
|
48
61
|
headers["Authorization"] = authorization
|
|
49
62
|
# 对于v3签名的RPC API,参数在request body中
|
|
50
|
-
|
|
63
|
+
path = path if not query_string else path + "?" + format(query_string)
|
|
64
|
+
return self._http(method, path, body=body_content, headers=headers)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AlidnsProvider(AliBaseProvider):
|
|
68
|
+
"""阿里云DNS Provider"""
|
|
51
69
|
|
|
52
70
|
def _split_zone_and_sub(self, domain):
|
|
53
71
|
# type: (str) -> tuple[str | None, str | None, str]
|
|
@@ -99,7 +117,7 @@ class AlidnsProvider(BaseProvider):
|
|
|
99
117
|
TTL=ttl,
|
|
100
118
|
Line=line,
|
|
101
119
|
**extra
|
|
102
|
-
)
|
|
120
|
+
) # fmt: skip
|
|
103
121
|
if data and data.get("RecordId"):
|
|
104
122
|
self.logger.info("Record created: %s", data)
|
|
105
123
|
return True
|
|
@@ -126,7 +144,7 @@ class AlidnsProvider(BaseProvider):
|
|
|
126
144
|
TTL=ttl,
|
|
127
145
|
Line=line or old_record.get("Line"),
|
|
128
146
|
**extra
|
|
129
|
-
)
|
|
147
|
+
) # fmt: skip
|
|
130
148
|
if data and data.get("RecordId"):
|
|
131
149
|
self.logger.info("Record updated: %s", data)
|
|
132
150
|
return True
|