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
ddns/provider/__init__.py
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
from ._base import SimpleProvider
|
|
3
|
+
from .alidns import AlidnsProvider
|
|
4
|
+
from .aliesa import AliesaProvider
|
|
5
|
+
from .callback import CallbackProvider
|
|
6
|
+
from .cloudflare import CloudflareProvider
|
|
7
|
+
from .debug import DebugProvider
|
|
8
|
+
from .dnscom import DnscomProvider
|
|
9
|
+
from .dnspod import DnspodProvider
|
|
10
|
+
from .dnspod_com import DnspodComProvider
|
|
11
|
+
from .edgeone import EdgeOneProvider
|
|
12
|
+
from .he import HeProvider
|
|
13
|
+
from .huaweidns import HuaweiDNSProvider
|
|
14
|
+
from .namesilo import NamesiloProvider
|
|
15
|
+
from .noip import NoipProvider
|
|
16
|
+
from .tencentcloud import TencentCloudProvider
|
|
17
|
+
|
|
18
|
+
__all__ = ["SimpleProvider", "get_provider_class"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_provider_class(provider_name):
|
|
22
|
+
# type: (str) -> type[SimpleProvider]
|
|
23
|
+
"""
|
|
24
|
+
获取指定的DNS提供商类
|
|
25
|
+
|
|
26
|
+
:param provider_name: 提供商名称
|
|
27
|
+
:return: 对应的DNS提供商类
|
|
28
|
+
"""
|
|
29
|
+
provider_name = str(provider_name).lower()
|
|
30
|
+
mapping = {
|
|
31
|
+
# dnspod.cn
|
|
32
|
+
"dnspod": DnspodProvider,
|
|
33
|
+
"dnspod_cn": DnspodProvider, # 兼容旧的dnspod_cn
|
|
34
|
+
# dnspod.com
|
|
35
|
+
"dnspod_com": DnspodComProvider,
|
|
36
|
+
"dnspod_global": DnspodComProvider, # 兼容旧的dnspod_global
|
|
37
|
+
# tencent cloud dnspod
|
|
38
|
+
"tencentcloud": TencentCloudProvider,
|
|
39
|
+
"tencent": TencentCloudProvider, # 兼容tencent
|
|
40
|
+
"qcloud": TencentCloudProvider, # 兼容qcloud
|
|
41
|
+
# tencent cloud edgeone
|
|
42
|
+
"edgeone": EdgeOneProvider,
|
|
43
|
+
"teo": EdgeOneProvider, # 兼容teo (EdgeOne产品的API名称)
|
|
44
|
+
"tencentedgeone": EdgeOneProvider, # 兼容tencentedgeone
|
|
45
|
+
# cloudflare
|
|
46
|
+
"cloudflare": CloudflareProvider,
|
|
47
|
+
# aliyun alidns
|
|
48
|
+
"alidns": AlidnsProvider,
|
|
49
|
+
"aliyun": AlidnsProvider, # 兼容aliyun
|
|
50
|
+
# aliyun esa
|
|
51
|
+
"aliesa": AliesaProvider,
|
|
52
|
+
"esa": AliesaProvider, # 兼容esa
|
|
53
|
+
# dns.com
|
|
54
|
+
"dnscom": DnscomProvider,
|
|
55
|
+
"51dns": DnscomProvider, # 兼容51dns
|
|
56
|
+
"dns_com": DnscomProvider, # 兼容dns_com
|
|
57
|
+
# he.net
|
|
58
|
+
"he": HeProvider,
|
|
59
|
+
"he_net": HeProvider, # 兼容he.net
|
|
60
|
+
# huawei
|
|
61
|
+
"huaweidns": HuaweiDNSProvider,
|
|
62
|
+
"huawei": HuaweiDNSProvider, # 兼容huawei
|
|
63
|
+
"huaweicloud": HuaweiDNSProvider,
|
|
64
|
+
# namesilo
|
|
65
|
+
"namesilo": NamesiloProvider,
|
|
66
|
+
"namesilo_com": NamesiloProvider, # 兼容namesilo.com
|
|
67
|
+
# no-ip
|
|
68
|
+
"noip": NoipProvider,
|
|
69
|
+
"no-ip": NoipProvider, # 兼容no-ip
|
|
70
|
+
"noip_com": NoipProvider, # 兼容noip.com
|
|
71
|
+
# callback
|
|
72
|
+
"callback": CallbackProvider,
|
|
73
|
+
"webhook": CallbackProvider, # 兼容
|
|
74
|
+
"http": CallbackProvider, # 兼容
|
|
75
|
+
# debug
|
|
76
|
+
"print": DebugProvider,
|
|
77
|
+
"debug": DebugProvider, # 兼容print
|
|
78
|
+
}
|
|
79
|
+
return mapping.get(provider_name) # type: ignore[return-value]
|
ddns/provider/_base.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
## SimpleProvider 简单DNS抽象基类
|
|
4
|
+
* set_record()
|
|
5
|
+
|
|
6
|
+
## BaseProvider 标准DNS抽象基类
|
|
7
|
+
定义所有 DNS 服务商 API 类应继承的抽象基类,统一接口,便于扩展适配多服务商。
|
|
8
|
+
Defines a unified interface to support extension and adaptation across providers.
|
|
9
|
+
* _query_zone_id
|
|
10
|
+
* _query_record
|
|
11
|
+
* _update_record
|
|
12
|
+
* _create_record
|
|
13
|
+
|
|
14
|
+
┌──────────────────────────────────────────────────┐
|
|
15
|
+
│ 调用 set_record(domain, value...) │
|
|
16
|
+
└──────────────────────────────────────────────────┘
|
|
17
|
+
│
|
|
18
|
+
▼
|
|
19
|
+
┌──────────────────────────────────────┐
|
|
20
|
+
│ 快速解析 是否包含 ~ 或 + 分隔符? │
|
|
21
|
+
└──────────────────────────────────────┘
|
|
22
|
+
│ │
|
|
23
|
+
[是,拆解成功] [否,无法拆解]
|
|
24
|
+
sub 和 main│ │ domain
|
|
25
|
+
▼ ▼
|
|
26
|
+
┌────────────────────────┐ ┌──────────────────────────┐
|
|
27
|
+
│ 查询 zone_id │ │ 自动循环解析 while: │
|
|
28
|
+
│ _query_zone_id(main) │ │ _query_zone_id(...) │
|
|
29
|
+
└────────────────────────┘ └──────────────────────────┘
|
|
30
|
+
│ │
|
|
31
|
+
▼ ▼
|
|
32
|
+
zone_id ←──────────────┬─── sub
|
|
33
|
+
▼
|
|
34
|
+
┌─────────────────────────────────────┐
|
|
35
|
+
│ 查询 record: │
|
|
36
|
+
│ _query_record(zone_id, sub, ...) │
|
|
37
|
+
└─────────────────────────────────────┘
|
|
38
|
+
│
|
|
39
|
+
┌─────────────┴────────────────┐
|
|
40
|
+
│ record_id 是否存在? │
|
|
41
|
+
└────────────┬─────────────────┘
|
|
42
|
+
│
|
|
43
|
+
┌──────────────┴─────────────┐
|
|
44
|
+
│ │
|
|
45
|
+
▼ ▼
|
|
46
|
+
┌─────────────────────┐ ┌─────────────────────┐
|
|
47
|
+
│ 更新记录 │ │ 创建记录 │
|
|
48
|
+
│ _update_record(...) │ │ _create_record(...) │
|
|
49
|
+
└─────────────────────┘ └─────────────────────┘
|
|
50
|
+
│ │
|
|
51
|
+
▼ ▼
|
|
52
|
+
┌───────────────────────────────┐
|
|
53
|
+
│ 返回操作结果 │
|
|
54
|
+
└───────────────────────────────┘
|
|
55
|
+
@author: NewFuture
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from abc import ABCMeta, abstractmethod
|
|
59
|
+
from json import loads as jsondecode, dumps as jsonencode
|
|
60
|
+
from logging import Logger, getLogger # noqa:F401 # type: ignore[no-redef]
|
|
61
|
+
from ..util.http import request, quote, urlencode
|
|
62
|
+
|
|
63
|
+
TYPE_FORM = "application/x-www-form-urlencoded"
|
|
64
|
+
TYPE_JSON = "application/json"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def encode_params(params):
|
|
68
|
+
# type: (dict|list|str|bytes|None) -> str
|
|
69
|
+
"""
|
|
70
|
+
编码参数为 URL 查询字符串,参数顺序会排序
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
params (dict|list|str|bytes|None): 参数字典、列表或字符串
|
|
74
|
+
Returns:
|
|
75
|
+
str: 编码后的查询字符串
|
|
76
|
+
"""
|
|
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)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SimpleProvider(object):
|
|
86
|
+
"""
|
|
87
|
+
简单DNS服务商接口的抽象基类, 必须实现 `set_record` 方法。
|
|
88
|
+
|
|
89
|
+
Abstract base class for all simple DNS provider APIs.
|
|
90
|
+
Subclasses must implement `set_record`.
|
|
91
|
+
|
|
92
|
+
* set_record(domain, value, record_type="A", ttl=None, line=None, **extra)
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
__metaclass__ = ABCMeta
|
|
96
|
+
|
|
97
|
+
# API endpoint domain (to be defined in subclass)
|
|
98
|
+
endpoint = "" # type: str # https://exampledns.com
|
|
99
|
+
# Content-Type for requests (to be defined in subclass)
|
|
100
|
+
content_type = TYPE_FORM # type: Literal["application/x-www-form-urlencoded"] | Literal["application/json"]
|
|
101
|
+
# 默认 accept 头部, 空则不设置
|
|
102
|
+
accept = TYPE_JSON # type: str | None
|
|
103
|
+
# Decode Response as JSON by default
|
|
104
|
+
decode_response = True # type: bool
|
|
105
|
+
# Description
|
|
106
|
+
remark = "Managed by [DDNS](https://ddns.newfuture.cc)"
|
|
107
|
+
|
|
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
|
|
110
|
+
"""
|
|
111
|
+
初始化服务商对象
|
|
112
|
+
|
|
113
|
+
Initialize provider instance.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
id (str): 身份认证 ID / Authentication ID
|
|
117
|
+
token (str): 密钥 / Authentication Token
|
|
118
|
+
proxy (list[str | None] | None): 代理配置,支持代理列表
|
|
119
|
+
options (dict): 其它参数 / Additional options
|
|
120
|
+
"""
|
|
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
|
+
|
|
131
|
+
self.options = options
|
|
132
|
+
name = self.__class__.__name__
|
|
133
|
+
self.logger = (logger or getLogger()).getChild(name)
|
|
134
|
+
|
|
135
|
+
self._zone_map = {} # type: dict[str, str]
|
|
136
|
+
self.logger.debug("%s initialized with: %s", self.__class__.__name__, id)
|
|
137
|
+
self._validate() # 验证身份认证信息
|
|
138
|
+
|
|
139
|
+
@abstractmethod
|
|
140
|
+
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
|
|
141
|
+
# type: (str, str, str, str | int | None, str | None, **object) -> bool
|
|
142
|
+
"""
|
|
143
|
+
设置 DNS 记录(创建或更新)
|
|
144
|
+
|
|
145
|
+
Set or update DNS record.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
domain (str): 完整域名
|
|
149
|
+
value (str): 新记录值
|
|
150
|
+
record_type (str): 记录类型
|
|
151
|
+
ttl (int | None): TTL 值,可选
|
|
152
|
+
line (str | None): 线路信息
|
|
153
|
+
extra (dict): 额外参数
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Any: 执行结果
|
|
157
|
+
"""
|
|
158
|
+
raise NotImplementedError("This set_record should be implemented by subclasses")
|
|
159
|
+
|
|
160
|
+
def _validate(self):
|
|
161
|
+
# type: () -> None
|
|
162
|
+
"""
|
|
163
|
+
验证身份认证信息是否填写
|
|
164
|
+
|
|
165
|
+
Validate authentication credentials.
|
|
166
|
+
"""
|
|
167
|
+
if not self.id:
|
|
168
|
+
raise ValueError("id must be configured")
|
|
169
|
+
if not self.token:
|
|
170
|
+
raise ValueError("token must be configured")
|
|
171
|
+
if not self.endpoint:
|
|
172
|
+
raise ValueError("API endpoint must be defined in {}".format(self.__class__.__name__))
|
|
173
|
+
|
|
174
|
+
def _http(self, method, url, params=None, body=None, queries=None, headers=None): # noqa: C901
|
|
175
|
+
# type: (str, str, dict[str,Any]|str|None, dict[str,Any]|str|None, dict[str,Any]|None, dict|None) -> Any
|
|
176
|
+
"""
|
|
177
|
+
发送 HTTP/HTTPS 请求,自动根据 API/url 选择协议。
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
method (str): 请求方法,如 GET、POST
|
|
181
|
+
url (str): 请求路径
|
|
182
|
+
params (dict[str, Any] | None): 请求参数,自动处理 query string 或者body
|
|
183
|
+
body (dict[str, Any] | str | None): 请求体内容
|
|
184
|
+
queries (dict[str, Any] | None): 查询参数,自动处理为 URL 查询字符串
|
|
185
|
+
headers (dict): 头部,可选
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Any: 解析后的响应内容
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
RuntimeError: 当响应状态码为400/401或5xx(服务器错误)时抛出异常
|
|
192
|
+
"""
|
|
193
|
+
method = method.upper()
|
|
194
|
+
|
|
195
|
+
# 简化参数处理逻辑
|
|
196
|
+
query_params = queries or {}
|
|
197
|
+
if params:
|
|
198
|
+
if method in ("GET", "DELETE"):
|
|
199
|
+
if isinstance(params, dict):
|
|
200
|
+
query_params.update(params)
|
|
201
|
+
else:
|
|
202
|
+
# params是字符串,直接作为查询字符串
|
|
203
|
+
url += ("&" if "?" in url else "?") + str(params)
|
|
204
|
+
params = None
|
|
205
|
+
elif body is None:
|
|
206
|
+
body = params
|
|
207
|
+
|
|
208
|
+
# 构建查询字符串
|
|
209
|
+
if len(query_params) > 0:
|
|
210
|
+
url += ("&" if "?" in url else "?") + encode_params(query_params)
|
|
211
|
+
|
|
212
|
+
# 构建完整URL
|
|
213
|
+
if not url.startswith("http://") and not url.startswith("https://"):
|
|
214
|
+
if not url.startswith("/") and self.endpoint.endswith("/"):
|
|
215
|
+
url = "/" + url
|
|
216
|
+
url = self.endpoint + url
|
|
217
|
+
|
|
218
|
+
# 记录请求日志
|
|
219
|
+
self.logger.info("%s %s", method, self._mask_sensitive_data(url))
|
|
220
|
+
|
|
221
|
+
# 处理请求体
|
|
222
|
+
body_data, headers = None, headers or {}
|
|
223
|
+
if body:
|
|
224
|
+
if "content-type" not in headers:
|
|
225
|
+
headers["content-type"] = self.content_type
|
|
226
|
+
body_data = self._encode_body(body)
|
|
227
|
+
self.logger.debug("body:\n%s", self._mask_sensitive_data(body_data))
|
|
228
|
+
|
|
229
|
+
# 处理headers
|
|
230
|
+
if self.accept and "accept" not in headers and "Accept" not in headers:
|
|
231
|
+
headers["accept"] = self.accept
|
|
232
|
+
if len(headers) > 2:
|
|
233
|
+
self.logger.debug("headers:\n%s", {k: self._mask_sensitive_data(v) for k, v in headers.items()})
|
|
234
|
+
|
|
235
|
+
# 直接传递代理列表给request函数
|
|
236
|
+
response = request(method, url, body_data, headers=headers, proxies=self._proxy, verify=self._ssl, retries=2)
|
|
237
|
+
# 处理响应
|
|
238
|
+
status_code = response.status
|
|
239
|
+
if not (200 <= status_code < 300):
|
|
240
|
+
self.logger.warning("response status: %s %s", status_code, response.reason)
|
|
241
|
+
|
|
242
|
+
res = response.body
|
|
243
|
+
# 针对客户端错误、认证/授权错误和服务器错误直接抛出异常
|
|
244
|
+
if status_code >= 500 or status_code in (400, 401, 403):
|
|
245
|
+
self.logger.error("HTTP error:\n%s", res)
|
|
246
|
+
if status_code == 400:
|
|
247
|
+
raise RuntimeError("参数错误 [400]: " + response.reason)
|
|
248
|
+
elif status_code == 401:
|
|
249
|
+
raise RuntimeError("认证失败 [401]: " + response.reason)
|
|
250
|
+
elif status_code == 403:
|
|
251
|
+
raise RuntimeError("禁止访问 [403]: " + response.reason)
|
|
252
|
+
else:
|
|
253
|
+
raise RuntimeError("服务器错误 [{}]: {}".format(status_code, response.reason))
|
|
254
|
+
|
|
255
|
+
self.logger.debug("response:\n%s", res)
|
|
256
|
+
if not self.decode_response:
|
|
257
|
+
return res
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
return jsondecode(res)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
self.logger.error("fail to decode response: %s", e)
|
|
263
|
+
return res
|
|
264
|
+
|
|
265
|
+
def _encode_body(self, data):
|
|
266
|
+
# type: (dict | list | str | bytes | None) -> str
|
|
267
|
+
"""
|
|
268
|
+
自动编码数据为字符串或字节, 根据 content_type 选择编码方式。
|
|
269
|
+
Args:
|
|
270
|
+
data (dict | list | str | bytes | None): 待编码数据
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
str | bytes | None: 编码后的数据
|
|
274
|
+
"""
|
|
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)
|
|
282
|
+
|
|
283
|
+
def _mask_sensitive_data(self, data):
|
|
284
|
+
# type: (str | bytes | None) -> str | bytes | None
|
|
285
|
+
"""
|
|
286
|
+
对敏感数据进行打码处理,用于日志输出,支持URL编码的敏感信息
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
data (str | bytes | None): 需要处理的数据
|
|
290
|
+
Returns:
|
|
291
|
+
str | bytes | None: 打码后的字符串
|
|
292
|
+
"""
|
|
293
|
+
if not data or not self.token:
|
|
294
|
+
return data
|
|
295
|
+
|
|
296
|
+
# 生成打码后的token
|
|
297
|
+
token_masked = self.token[:2] + "***" + self.token[-2:] if len(self.token) > 4 else "***"
|
|
298
|
+
token_encoded = quote(self.token, safe="")
|
|
299
|
+
|
|
300
|
+
if isinstance(data, bytes): # 处理字节数据
|
|
301
|
+
return data.replace(self.token.encode(), token_masked.encode()).replace(
|
|
302
|
+
token_encoded.encode(), token_masked.encode()
|
|
303
|
+
)
|
|
304
|
+
if hasattr(data, "replace"): # 处理字符串数据
|
|
305
|
+
return data.replace(self.token, token_masked).replace(token_encoded, token_masked)
|
|
306
|
+
return data
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class BaseProvider(SimpleProvider):
|
|
310
|
+
"""
|
|
311
|
+
标准DNS服务商接口的抽象基类
|
|
312
|
+
|
|
313
|
+
Abstract base class for all standard DNS provider APIs.
|
|
314
|
+
Subclasses must implement the abstract methods to support various providers.
|
|
315
|
+
|
|
316
|
+
* _query_zone_id()
|
|
317
|
+
* _query_record_id()
|
|
318
|
+
* _update_record()
|
|
319
|
+
* _create_record()
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
|
|
323
|
+
# type: (str, str, str, str | int | None, str | None, **Any) -> bool
|
|
324
|
+
"""
|
|
325
|
+
设置 DNS 记录(创建或更新)
|
|
326
|
+
|
|
327
|
+
Set or update DNS record.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
domain (str): 完整域名
|
|
331
|
+
value (str): 新记录值
|
|
332
|
+
record_type (str): 记录类型
|
|
333
|
+
ttl (int | None): TTL 值,可选
|
|
334
|
+
line (str | None): 线路信息
|
|
335
|
+
extra (dict): 额外参数
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
bool: 执行结果
|
|
339
|
+
"""
|
|
340
|
+
domain = domain.lower()
|
|
341
|
+
self.logger.info("%s => %s(%s)", domain, value, record_type)
|
|
342
|
+
sub, main = _split_custom_domain(domain)
|
|
343
|
+
try:
|
|
344
|
+
if sub is not None:
|
|
345
|
+
# 使用自定义分隔符格式
|
|
346
|
+
zone_id = self.get_zone_id(main)
|
|
347
|
+
else:
|
|
348
|
+
# 自动分析域名
|
|
349
|
+
zone_id, sub, main = self._split_zone_and_sub(domain)
|
|
350
|
+
|
|
351
|
+
self.logger.info("sub: %s, main: %s(id=%s)", sub, main, zone_id)
|
|
352
|
+
if not zone_id or sub is None:
|
|
353
|
+
self.logger.critical("找不到 zone_id 或 subdomain: %s", domain)
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
# 查询现有记录
|
|
357
|
+
record = self._query_record(zone_id, sub, main, record_type=record_type, line=line, extra=extra)
|
|
358
|
+
|
|
359
|
+
# 更新或创建记录
|
|
360
|
+
if record:
|
|
361
|
+
self.logger.info("Found existing record: %s", record)
|
|
362
|
+
return self._update_record(zone_id, record, value, record_type, ttl=ttl, line=line, extra=extra)
|
|
363
|
+
else:
|
|
364
|
+
self.logger.warning("No existing record found, creating new one")
|
|
365
|
+
return self._create_record(zone_id, sub, main, value, record_type, ttl=ttl, line=line, extra=extra)
|
|
366
|
+
except Exception as e:
|
|
367
|
+
self.logger.exception("Error setting record for %s: %s", domain, e)
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
def get_zone_id(self, domain):
|
|
371
|
+
# type: (str) -> str | None
|
|
372
|
+
"""
|
|
373
|
+
查询指定域名对应的 zone_id
|
|
374
|
+
|
|
375
|
+
Get zone_id for the domain.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
domain (str): 主域名 / main name
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
str | None: 区域 ID / Zone identifier
|
|
382
|
+
"""
|
|
383
|
+
if domain in self._zone_map:
|
|
384
|
+
return self._zone_map[domain]
|
|
385
|
+
zone_id = self._query_zone_id(domain)
|
|
386
|
+
if zone_id:
|
|
387
|
+
self._zone_map[domain] = zone_id
|
|
388
|
+
return zone_id
|
|
389
|
+
|
|
390
|
+
@abstractmethod
|
|
391
|
+
def _query_zone_id(self, domain):
|
|
392
|
+
# type: (str) -> str | None
|
|
393
|
+
"""
|
|
394
|
+
查询主域名的 zone ID
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
domain (str): 主域名
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
str | None: Zone ID
|
|
401
|
+
"""
|
|
402
|
+
return domain
|
|
403
|
+
|
|
404
|
+
@abstractmethod
|
|
405
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
406
|
+
# type: (str, str, str, str, str | None, dict) -> Any
|
|
407
|
+
"""
|
|
408
|
+
查询 DNS 记录 ID
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
zone_id (str): 区域 ID
|
|
412
|
+
subdomain (str): 子域名
|
|
413
|
+
main_domain (str): 主域名
|
|
414
|
+
record_type (str): 记录类型,例如 A、AAAA
|
|
415
|
+
line (str | None): 线路选项,可选
|
|
416
|
+
extra (dict): 额外参数
|
|
417
|
+
Returns:
|
|
418
|
+
Any | None: 记录
|
|
419
|
+
"""
|
|
420
|
+
raise NotImplementedError("This _query_record should be implemented by subclasses")
|
|
421
|
+
|
|
422
|
+
@abstractmethod
|
|
423
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
424
|
+
# type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
|
|
425
|
+
"""
|
|
426
|
+
创建新 DNS 记录
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
zone_id (str): 区域 ID
|
|
430
|
+
subdomain (str): 子域名
|
|
431
|
+
main_domain (str): 主域名
|
|
432
|
+
value (str): 记录值
|
|
433
|
+
record_type (str): 类型,如 A
|
|
434
|
+
ttl (int | None): TTL 可选
|
|
435
|
+
line (str | None): 线路选项
|
|
436
|
+
extra (dict | None): 额外字段
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Any: 操作结果
|
|
440
|
+
"""
|
|
441
|
+
raise NotImplementedError("This _create_record should be implemented by subclasses")
|
|
442
|
+
|
|
443
|
+
@abstractmethod
|
|
444
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
445
|
+
# type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
|
|
446
|
+
"""
|
|
447
|
+
更新已有 DNS 记录
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
zone_id (str): 区域 ID
|
|
451
|
+
old_record (dict): 旧记录信息
|
|
452
|
+
value (str): 新的记录值
|
|
453
|
+
record_type (str): 类型
|
|
454
|
+
ttl (int | None): TTL
|
|
455
|
+
line (str | None): 线路
|
|
456
|
+
extra (dict | None): 额外参数
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
bool: 操作结果
|
|
460
|
+
"""
|
|
461
|
+
raise NotImplementedError("This _update_record should be implemented by subclasses")
|
|
462
|
+
|
|
463
|
+
def _split_zone_and_sub(self, domain):
|
|
464
|
+
# type: (str) -> tuple[str | None, str | None, str ]
|
|
465
|
+
"""
|
|
466
|
+
从完整域名拆分主域名和子域名
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
domain (str): 完整域名
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
(zone_id, sub): 元组
|
|
473
|
+
"""
|
|
474
|
+
domain_split = domain.split(".")
|
|
475
|
+
zone_id = None
|
|
476
|
+
index = 2
|
|
477
|
+
main = ""
|
|
478
|
+
while not zone_id and index <= len(domain_split):
|
|
479
|
+
main = ".".join(domain_split[-index:])
|
|
480
|
+
zone_id = self.get_zone_id(main)
|
|
481
|
+
index += 1
|
|
482
|
+
if zone_id:
|
|
483
|
+
sub = ".".join(domain_split[: -index + 1]) or "@"
|
|
484
|
+
self.logger.debug("zone_id: %s, sub: %s", zone_id, sub)
|
|
485
|
+
return zone_id, sub, main
|
|
486
|
+
return None, None, main
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _split_custom_domain(domain):
|
|
490
|
+
# type: (str) -> tuple[str | None, str]
|
|
491
|
+
"""
|
|
492
|
+
拆分支持 ~ 或 + 的自定义格式域名为 (子域, 主域)
|
|
493
|
+
|
|
494
|
+
如 sub~example.com => ('sub', 'example.com')
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
(sub, main): 子域 + 主域
|
|
498
|
+
"""
|
|
499
|
+
for sep in ("~", "+"):
|
|
500
|
+
if sep in domain:
|
|
501
|
+
sub, main = domain.split(sep, 1)
|
|
502
|
+
return sub, main
|
|
503
|
+
return None, domain
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def join_domain(sub, main):
|
|
507
|
+
# type: (str | None, str) -> str
|
|
508
|
+
"""
|
|
509
|
+
合并子域名和主域名为完整域名
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
sub (str | None): 子域名
|
|
513
|
+
main (str): 主域名
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
str: 完整域名
|
|
517
|
+
"""
|
|
518
|
+
sub = sub and sub.strip(".").strip().lower()
|
|
519
|
+
main = main and main.strip(".").strip().lower()
|
|
520
|
+
if not sub or sub == "@":
|
|
521
|
+
if not main:
|
|
522
|
+
raise ValueError("Both sub and main cannot be empty")
|
|
523
|
+
return main
|
|
524
|
+
if not main:
|
|
525
|
+
return sub
|
|
526
|
+
return "{}.{}".format(sub, main)
|