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/__builtins__.pyi +6 -0
- ddns/__init__.py +2 -9
- ddns/__main__.py +108 -144
- ddns/cache.py +183 -0
- ddns/ip.py +145 -0
- ddns/provider/__init__.py +79 -0
- ddns/provider/_base.py +526 -0
- ddns/provider/_signature.py +113 -0
- ddns/provider/alidns.py +145 -176
- ddns/provider/aliesa.py +130 -0
- ddns/provider/callback.py +66 -104
- ddns/provider/cloudflare.py +87 -151
- ddns/provider/debug.py +19 -0
- ddns/provider/dnscom.py +91 -168
- ddns/provider/dnspod.py +102 -174
- ddns/provider/dnspod_com.py +11 -8
- ddns/provider/edgeone.py +83 -0
- ddns/provider/he.py +41 -78
- ddns/provider/huaweidns.py +134 -256
- ddns/provider/namesilo.py +159 -0
- ddns/provider/noip.py +101 -0
- ddns/provider/tencentcloud.py +194 -0
- ddns/util/comment.py +88 -0
- ddns/util/fileio.py +113 -0
- ddns/util/http.py +322 -0
- ddns/util/try_run.py +37 -0
- ddns-4.1.0.dist-info/METADATA +327 -0
- ddns-4.1.0.dist-info/RECORD +33 -0
- ddns/util/cache.py +0 -139
- ddns/util/config.py +0 -317
- ddns/util/ip.py +0 -96
- ddns-4.0.1.dist-info/METADATA +0 -326
- ddns-4.0.1.dist-info/RECORD +0 -21
- {ddns-4.0.1.dist-info → ddns-4.1.0.dist-info}/WHEEL +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0.dist-info}/entry_points.txt +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
@@ -2,182 +2,151 @@
|
|
|
2
2
|
"""
|
|
3
3
|
AliDNS API
|
|
4
4
|
阿里DNS解析操作库
|
|
5
|
-
|
|
6
|
-
@author: New Future
|
|
5
|
+
@author: NewFuture
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def get_records(domain, **conditions):
|
|
109
|
-
"""
|
|
110
|
-
获取记录ID
|
|
111
|
-
返回满足条件的所有记录[]
|
|
112
|
-
https://help.aliyun.com/document_detail/29776.html
|
|
113
|
-
TODO 大于500翻页
|
|
114
|
-
"""
|
|
115
|
-
if not hasattr(get_records, "records"):
|
|
116
|
-
get_records.records = {} # "静态变量"存储已查询过的id
|
|
117
|
-
get_records.keys = ("RecordId", "RR", "Type", "Line",
|
|
118
|
-
"Locked", "Status", "Priority", "Value")
|
|
119
|
-
|
|
120
|
-
if domain not in get_records.records:
|
|
121
|
-
get_records.records[domain] = {}
|
|
122
|
-
data = request(Action="DescribeDomainRecords",
|
|
123
|
-
DomainName=domain, PageSize=500)
|
|
124
|
-
if data:
|
|
125
|
-
for record in data.get('DomainRecords').get('Record'):
|
|
126
|
-
get_records.records[domain][record["RecordId"]] = {
|
|
127
|
-
k: v for (k, v) in record.items() if k in get_records.keys}
|
|
128
|
-
records = {}
|
|
129
|
-
for (rid, record) in get_records.records[domain].items():
|
|
130
|
-
for (k, value) in conditions.items():
|
|
131
|
-
if record.get(k) != value:
|
|
132
|
-
break
|
|
133
|
-
else: # for else push
|
|
134
|
-
records[rid] = record
|
|
135
|
-
return records
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def update_record(domain, value, record_type='A'):
|
|
139
|
-
"""
|
|
140
|
-
更新记录
|
|
141
|
-
update
|
|
142
|
-
https://help.aliyun.com/document_detail/29774.html
|
|
143
|
-
add
|
|
144
|
-
https://help.aliyun.com/document_detail/29772.html?
|
|
145
|
-
"""
|
|
146
|
-
debug(">>>>>%s(%s)", domain, record_type)
|
|
147
|
-
sub, main = get_domain_info(domain)
|
|
148
|
-
if not sub:
|
|
149
|
-
raise Exception("invalid domain: [ %s ] " % domain)
|
|
150
|
-
|
|
151
|
-
records = get_records(main, RR=sub, Type=record_type)
|
|
152
|
-
result = {}
|
|
153
|
-
|
|
154
|
-
if records:
|
|
155
|
-
for (rid, record) in records.items():
|
|
156
|
-
if record["Value"] != value:
|
|
157
|
-
debug(sub, record)
|
|
158
|
-
res = request(Action="UpdateDomainRecord", RecordId=rid,
|
|
159
|
-
Value=value, RR=sub, Type=record_type, TTL=Config.TTL)
|
|
160
|
-
if res:
|
|
161
|
-
# update records
|
|
162
|
-
get_records.records[main][rid]["Value"] = value
|
|
163
|
-
result[rid] = res
|
|
164
|
-
else:
|
|
165
|
-
result[rid] = "update fail!\n" + str(res)
|
|
166
|
-
else:
|
|
167
|
-
result[rid] = domain
|
|
168
|
-
else: # https://help.aliyun.com/document_detail/29772.html
|
|
169
|
-
res = request(Action="AddDomainRecord", DomainName=main,
|
|
170
|
-
Value=value, RR=sub, Type=record_type, TTL=Config.TTL)
|
|
171
|
-
if res:
|
|
172
|
-
# update records INFO
|
|
173
|
-
rid = res.get('RecordId')
|
|
174
|
-
get_records.records[main][rid] = {
|
|
175
|
-
'Value': value,
|
|
176
|
-
"RecordId": rid,
|
|
177
|
-
"RR": sub,
|
|
178
|
-
"Type": record_type
|
|
179
|
-
}
|
|
180
|
-
result = res
|
|
8
|
+
from time import gmtime, strftime, time
|
|
9
|
+
|
|
10
|
+
from ._base import TYPE_FORM, BaseProvider, encode_params, join_domain
|
|
11
|
+
from ._signature import hmac_sha256_authorization, sha256_hash
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AliBaseProvider(BaseProvider):
|
|
15
|
+
"""阿里云基础Provider,提供通用的_request方法"""
|
|
16
|
+
|
|
17
|
+
endpoint = "https://alidns.aliyuncs.com"
|
|
18
|
+
content_type = TYPE_FORM # 阿里云DNS API使用表单格式
|
|
19
|
+
api_version = "2015-01-09" # API版本,v3签名需要
|
|
20
|
+
|
|
21
|
+
def _request(self, action, method="POST", **params):
|
|
22
|
+
# type: (str, str, **(Any)) -> dict
|
|
23
|
+
"""Aliyun v3 https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature"""
|
|
24
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
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 = "/"
|
|
36
|
+
content_hash = sha256_hash(body_content)
|
|
37
|
+
# 构造请求头部
|
|
38
|
+
headers = {
|
|
39
|
+
"host": self.endpoint.split("://", 1)[1].strip("/"),
|
|
40
|
+
"content-type": self.content_type,
|
|
41
|
+
"x-acs-action": action,
|
|
42
|
+
"x-acs-content-sha256": content_hash,
|
|
43
|
+
"x-acs-date": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()),
|
|
44
|
+
"x-acs-signature-nonce": str(hash(time()))[2:],
|
|
45
|
+
"x-acs-version": self.api_version,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# 使用通用签名函数
|
|
49
|
+
authorization = hmac_sha256_authorization(
|
|
50
|
+
secret_key=self.token,
|
|
51
|
+
method=method,
|
|
52
|
+
path=path,
|
|
53
|
+
query=query_string,
|
|
54
|
+
headers=headers,
|
|
55
|
+
body_hash=content_hash,
|
|
56
|
+
signing_string_format="ACS3-HMAC-SHA256\n{HashedCanonicalRequest}",
|
|
57
|
+
authorization_format=(
|
|
58
|
+
"ACS3-HMAC-SHA256 Credential=" + self.id + ",SignedHeaders={SignedHeaders},Signature={Signature}"
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
headers["Authorization"] = authorization
|
|
62
|
+
# 对于v3签名的RPC API,参数在request body中
|
|
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"""
|
|
69
|
+
|
|
70
|
+
def _split_zone_and_sub(self, domain):
|
|
71
|
+
# type: (str) -> tuple[str | None, str | None, str]
|
|
72
|
+
"""
|
|
73
|
+
AliDNS 支持直接查询主域名和RR,无需循环查询。
|
|
74
|
+
返回没有DomainId,用DomainName代替
|
|
75
|
+
https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-getmaindomainname
|
|
76
|
+
"""
|
|
77
|
+
res = self._request("GetMainDomainName", InputString=domain)
|
|
78
|
+
sub, main = res.get("RR"), res.get("DomainName")
|
|
79
|
+
return (main, sub, main or domain)
|
|
80
|
+
|
|
81
|
+
def _query_zone_id(self, domain):
|
|
82
|
+
"""调用_split_zone_and_sub可直接获取,无需调用_query_zone_id"""
|
|
83
|
+
raise NotImplementedError("_split_zone_and_sub is used to get zone_id")
|
|
84
|
+
|
|
85
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
86
|
+
"""https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-describesubdomainrecords"""
|
|
87
|
+
sub = join_domain(subdomain, main_domain)
|
|
88
|
+
data = self._request(
|
|
89
|
+
"DescribeSubDomainRecords",
|
|
90
|
+
SubDomain=sub, # aliyun API要求SubDomain为完整域名
|
|
91
|
+
DomainName=main_domain,
|
|
92
|
+
Type=record_type,
|
|
93
|
+
Line=line,
|
|
94
|
+
PageSize=500,
|
|
95
|
+
Lang=extra.get("Lang"), # 默认中文
|
|
96
|
+
Status=extra.get("Status"), # 默认全部状态
|
|
97
|
+
)
|
|
98
|
+
records = data.get("DomainRecords", {}).get("Record", [])
|
|
99
|
+
if not records:
|
|
100
|
+
self.logger.warning(
|
|
101
|
+
"No records found for [%s] with %s <%s> (line: %s)", zone_id, subdomain, record_type, line
|
|
102
|
+
)
|
|
103
|
+
elif not isinstance(records, list):
|
|
104
|
+
self.logger.error("Invalid records format: %s", records)
|
|
181
105
|
else:
|
|
182
|
-
|
|
183
|
-
|
|
106
|
+
return next((r for r in records), None)
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
110
|
+
"""https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-adddomainrecord"""
|
|
111
|
+
data = self._request(
|
|
112
|
+
"AddDomainRecord",
|
|
113
|
+
DomainName=main_domain,
|
|
114
|
+
RR=subdomain,
|
|
115
|
+
Value=value,
|
|
116
|
+
Type=record_type,
|
|
117
|
+
TTL=ttl,
|
|
118
|
+
Line=line,
|
|
119
|
+
**extra
|
|
120
|
+
) # fmt: skip
|
|
121
|
+
if data and data.get("RecordId"):
|
|
122
|
+
self.logger.info("Record created: %s", data)
|
|
123
|
+
return True
|
|
124
|
+
self.logger.error("Failed to create record: %s", data)
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
128
|
+
"""https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-updatedomainrecord"""
|
|
129
|
+
# 阿里云DNS update新旧值不能一样,先判断是否发生变化
|
|
130
|
+
if (
|
|
131
|
+
old_record.get("Value") == value
|
|
132
|
+
and old_record.get("Type") == record_type
|
|
133
|
+
and (not ttl or old_record.get("TTL") == ttl)
|
|
134
|
+
):
|
|
135
|
+
domain = join_domain(old_record.get("RR"), old_record.get("DomainName"))
|
|
136
|
+
self.logger.warning("No changes detected, skipping update for record: %s", domain)
|
|
137
|
+
return True
|
|
138
|
+
data = self._request(
|
|
139
|
+
"UpdateDomainRecord",
|
|
140
|
+
RecordId=old_record.get("RecordId"),
|
|
141
|
+
Value=value,
|
|
142
|
+
RR=old_record.get("RR"),
|
|
143
|
+
Type=record_type,
|
|
144
|
+
TTL=ttl,
|
|
145
|
+
Line=line or old_record.get("Line"),
|
|
146
|
+
**extra
|
|
147
|
+
) # fmt: skip
|
|
148
|
+
if data and data.get("RecordId"):
|
|
149
|
+
self.logger.info("Record updated: %s", data)
|
|
150
|
+
return True
|
|
151
|
+
self.logger.error("Failed to update record: %s", data)
|
|
152
|
+
return False
|
ddns/provider/aliesa.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
AliESA API
|
|
4
|
+
阿里云边缘安全加速(ESA) DNS 解析操作库
|
|
5
|
+
@author: NewFuture, GitHub Copilot
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from time import strftime
|
|
9
|
+
|
|
10
|
+
from ._base import TYPE_JSON, join_domain
|
|
11
|
+
from .alidns import AliBaseProvider
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AliesaProvider(AliBaseProvider):
|
|
15
|
+
"""阿里云边缘安全加速(ESA) DNS Provider"""
|
|
16
|
+
|
|
17
|
+
endpoint = "https://esa.cn-hangzhou.aliyuncs.com"
|
|
18
|
+
api_version = "2024-09-10" # ESA API版本
|
|
19
|
+
content_type = TYPE_JSON
|
|
20
|
+
remark = "Managed by DDNS %s" % strftime("%Y-%m-%d %H:%M:%S")
|
|
21
|
+
|
|
22
|
+
def _query_zone_id(self, domain):
|
|
23
|
+
# type: (str) -> str | None
|
|
24
|
+
"""
|
|
25
|
+
查询站点ID
|
|
26
|
+
https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listsites
|
|
27
|
+
"""
|
|
28
|
+
res = self._request(method="GET", action="ListSites", SiteName=domain, PageSize=500)
|
|
29
|
+
sites = res.get("Sites", [])
|
|
30
|
+
|
|
31
|
+
for site in sites:
|
|
32
|
+
if site.get("SiteName") == domain:
|
|
33
|
+
site_id = site.get("SiteId")
|
|
34
|
+
self.logger.debug("Found site ID %s for domain %s", site_id, domain)
|
|
35
|
+
return site_id
|
|
36
|
+
|
|
37
|
+
self.logger.error("Site not found for domain: %s", domain)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
41
|
+
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
42
|
+
"""
|
|
43
|
+
查询DNS记录
|
|
44
|
+
https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listrecords
|
|
45
|
+
"""
|
|
46
|
+
full_domain = join_domain(subdomain, main_domain)
|
|
47
|
+
res = self._request(
|
|
48
|
+
method="GET",
|
|
49
|
+
action="ListRecords",
|
|
50
|
+
SiteId=int(zone_id),
|
|
51
|
+
RecordName=full_domain,
|
|
52
|
+
Type=self._get_type(record_type),
|
|
53
|
+
RecordMatchType="exact", # 精确匹配
|
|
54
|
+
PageSize=100,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
records = res.get("Records", [])
|
|
58
|
+
if len(records) == 0:
|
|
59
|
+
self.logger.warning("No records found for [%s] with %s <%s>", zone_id, full_domain, record_type)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
# 返回第一个匹配的记录
|
|
63
|
+
record = records[0]
|
|
64
|
+
self.logger.debug("Found record: %s", record)
|
|
65
|
+
return record
|
|
66
|
+
|
|
67
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
68
|
+
# type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
|
|
69
|
+
"""
|
|
70
|
+
创建DNS记录
|
|
71
|
+
https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord
|
|
72
|
+
"""
|
|
73
|
+
full_domain = join_domain(subdomain, main_domain)
|
|
74
|
+
extra["Comment"] = extra.get("Comment", self.remark)
|
|
75
|
+
extra["BizName"] = extra.get("BizName", "web")
|
|
76
|
+
extra["Proxied"] = extra.get("Proxied", True)
|
|
77
|
+
data = self._request(
|
|
78
|
+
method="POST",
|
|
79
|
+
action="CreateRecord",
|
|
80
|
+
SiteId=int(zone_id),
|
|
81
|
+
RecordName=full_domain,
|
|
82
|
+
Type=self._get_type(record_type),
|
|
83
|
+
Data={"Value": value},
|
|
84
|
+
Ttl=ttl or 1,
|
|
85
|
+
**extra
|
|
86
|
+
) # fmt: skip
|
|
87
|
+
|
|
88
|
+
if data and data.get("RecordId"):
|
|
89
|
+
self.logger.info("Record created: %s", data)
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
self.logger.error("Failed to create record: %s", data)
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
96
|
+
# type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
|
|
97
|
+
"""
|
|
98
|
+
更新DNS记录
|
|
99
|
+
https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updaterecord
|
|
100
|
+
"""
|
|
101
|
+
# 检查是否需要更新
|
|
102
|
+
if (
|
|
103
|
+
old_record.get("Data", {}).get("Value") == value
|
|
104
|
+
and old_record.get("RecordType") == self._get_type(record_type)
|
|
105
|
+
and (not ttl or old_record.get("Ttl") == ttl)
|
|
106
|
+
):
|
|
107
|
+
self.logger.warning("No changes detected, skipping update for record: %s", old_record.get("RecordName"))
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
extra["Comment"] = extra.get("Comment", self.remark)
|
|
111
|
+
extra["Proxied"] = extra.get("Proxied", old_record.get("Proxied"))
|
|
112
|
+
data = self._request(
|
|
113
|
+
method="POST",
|
|
114
|
+
action="UpdateRecord",
|
|
115
|
+
RecordId=old_record.get("RecordId"),
|
|
116
|
+
Data={"Value": value},
|
|
117
|
+
Ttl=ttl,
|
|
118
|
+
**extra
|
|
119
|
+
) # fmt: skip
|
|
120
|
+
|
|
121
|
+
if data and data.get("RecordId"):
|
|
122
|
+
self.logger.info("Record updated: %s", data)
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
self.logger.error("Failed to update record: %s", data)
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def _get_type(self, record_type):
|
|
129
|
+
# type: (str) -> str
|
|
130
|
+
return "A/AAAA" if record_type in ("A", "AAAA") else record_type
|