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/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
|
ddns/provider/callback.py
CHANGED
|
@@ -5,6 +5,7 @@ Custom Callback API
|
|
|
5
5
|
|
|
6
6
|
@author: 老周部落, NewFuture
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
from ._base import TYPE_JSON, SimpleProvider
|
|
9
10
|
from time import time
|
|
10
11
|
from json import loads as jsondecode
|
|
@@ -16,7 +17,7 @@ class CallbackProvider(SimpleProvider):
|
|
|
16
17
|
Generic custom callback provider, supports GET/POST arbitrary API.
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
endpoint = "" # CallbackProvider uses id as URL, no fixed API endpoint
|
|
20
21
|
content_type = TYPE_JSON
|
|
21
22
|
decode_response = False # Callback response is not JSON, it's a custom response
|
|
22
23
|
|
|
@@ -26,9 +27,8 @@ class CallbackProvider(SimpleProvider):
|
|
|
26
27
|
Send custom callback request, support GET/POST
|
|
27
28
|
"""
|
|
28
29
|
self.logger.info("%s => %s(%s)", domain, value, record_type)
|
|
29
|
-
url = self.
|
|
30
|
-
token = self.
|
|
31
|
-
headers = {"User-Agent": "DDNS/{0} (ddns@newfuture.cc)".format(self.version)}
|
|
30
|
+
url = self.id # 直接用 id 作为 url
|
|
31
|
+
token = self.token # token 作为 POST 参数
|
|
32
32
|
extra.update(
|
|
33
33
|
{
|
|
34
34
|
"__DOMAIN__": domain,
|
|
@@ -51,7 +51,7 @@ class CallbackProvider(SimpleProvider):
|
|
|
51
51
|
params[k] = self._replace_vars(v, extra)
|
|
52
52
|
|
|
53
53
|
try:
|
|
54
|
-
res = self._http(method, url, body=params
|
|
54
|
+
res = self._http(method, url, body=params)
|
|
55
55
|
if res is not None:
|
|
56
56
|
self.logger.info("Callback result: %s", res)
|
|
57
57
|
return True
|
|
@@ -72,8 +72,8 @@ class CallbackProvider(SimpleProvider):
|
|
|
72
72
|
return string
|
|
73
73
|
|
|
74
74
|
def _validate(self):
|
|
75
|
-
# CallbackProvider uses
|
|
76
|
-
if not self.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
# CallbackProvider uses id as URL, not as regular ID
|
|
76
|
+
if self.endpoint or (not self.id or "://" not in self.id):
|
|
77
|
+
# 如果 endpoint 已经设置,或者 id 不是有效的 URL,则抛出异常
|
|
78
|
+
self.logger.critical("endpoint [%s] or id [%s] 必须是有效的URL", self.endpoint, self.id)
|
|
79
|
+
raise ValueError("endpoint or id must be configured with URL")
|
ddns/provider/cloudflare.py
CHANGED
|
@@ -4,30 +4,30 @@ CloudFlare API
|
|
|
4
4
|
@author: TongYifan, NewFuture
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from ._base import
|
|
7
|
+
from ._base import TYPE_JSON, BaseProvider, join_domain
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class CloudflareProvider(BaseProvider):
|
|
11
|
-
|
|
11
|
+
endpoint = "https://api.cloudflare.com"
|
|
12
12
|
content_type = TYPE_JSON
|
|
13
13
|
|
|
14
14
|
def _validate(self):
|
|
15
|
-
if not self.
|
|
15
|
+
if not self.token:
|
|
16
16
|
raise ValueError("token must be configured")
|
|
17
|
-
if self.
|
|
17
|
+
if self.id:
|
|
18
18
|
# must be email for Cloudflare API v4
|
|
19
|
-
if "@" not in self.
|
|
19
|
+
if "@" not in self.id:
|
|
20
20
|
self.logger.critical("ID 必须为空或有效的邮箱地址")
|
|
21
21
|
raise ValueError("ID must be a valid email or Empty for Cloudflare API v4")
|
|
22
22
|
|
|
23
23
|
def _request(self, method, action, **params):
|
|
24
24
|
"""发送请求数据"""
|
|
25
25
|
headers = {}
|
|
26
|
-
if self.
|
|
27
|
-
headers["X-Auth-Email"] = self.
|
|
28
|
-
headers["X-Auth-Key"] = self.
|
|
26
|
+
if self.id:
|
|
27
|
+
headers["X-Auth-Email"] = self.id
|
|
28
|
+
headers["X-Auth-Key"] = self.token
|
|
29
29
|
else:
|
|
30
|
-
headers["Authorization"] = "Bearer " + self.
|
|
30
|
+
headers["Authorization"] = "Bearer " + self.token
|
|
31
31
|
|
|
32
32
|
params = {k: v for k, v in params.items() if v is not None} # 过滤掉None参数
|
|
33
33
|
data = self._http(method, "/client/v4/zones" + action, headers=headers, params=params)
|
|
@@ -92,7 +92,7 @@ class CloudflareProvider(BaseProvider):
|
|
|
92
92
|
content=value,
|
|
93
93
|
ttl=ttl,
|
|
94
94
|
**extra
|
|
95
|
-
)
|
|
95
|
+
) # fmt: skip
|
|
96
96
|
self.logger.debug("Record updated: %s", data)
|
|
97
97
|
if data:
|
|
98
98
|
return True
|
ddns/provider/debug.py
CHANGED
ddns/provider/dnscom.py
CHANGED
|
@@ -5,10 +5,11 @@ www.51dns.com (原dns.com)
|
|
|
5
5
|
@author: Bigjin<i@bigjin.com>, NewFuture
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from ._base import BaseProvider, TYPE_FORM
|
|
9
8
|
from hashlib import md5
|
|
10
9
|
from time import time
|
|
11
10
|
|
|
11
|
+
from ._base import TYPE_FORM, BaseProvider, encode_params
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
class DnscomProvider(BaseProvider):
|
|
14
15
|
"""
|
|
@@ -16,7 +17,7 @@ class DnscomProvider(BaseProvider):
|
|
|
16
17
|
https://www.51dns.com/document/api/index.html
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
endpoint = "https://www.51dns.com"
|
|
20
21
|
content_type = TYPE_FORM
|
|
21
22
|
|
|
22
23
|
def _validate(self):
|
|
@@ -31,12 +32,12 @@ class DnscomProvider(BaseProvider):
|
|
|
31
32
|
params = {k: v for k, v in params.items() if v is not None}
|
|
32
33
|
params.update(
|
|
33
34
|
{
|
|
34
|
-
"apiKey": self.
|
|
35
|
+
"apiKey": self.id,
|
|
35
36
|
"timestamp": time(), # 时间戳
|
|
36
37
|
}
|
|
37
38
|
)
|
|
38
|
-
query =
|
|
39
|
-
sign = md5((query + self.
|
|
39
|
+
query = encode_params(params)
|
|
40
|
+
sign = md5((query + self.token).encode("utf-8")).hexdigest()
|
|
40
41
|
params["hash"] = sign
|
|
41
42
|
return params
|
|
42
43
|
|
|
@@ -82,7 +83,7 @@ class DnscomProvider(BaseProvider):
|
|
|
82
83
|
TTL=ttl,
|
|
83
84
|
viewID=line,
|
|
84
85
|
**extra
|
|
85
|
-
)
|
|
86
|
+
) # fmt: skip
|
|
86
87
|
if res and res.get("recordID"):
|
|
87
88
|
self.logger.info("Record created: %s", res)
|
|
88
89
|
return True
|
ddns/provider/dnspod.py
CHANGED
|
@@ -14,7 +14,7 @@ class DnspodProvider(BaseProvider):
|
|
|
14
14
|
DNSPOD 接口解析操作库
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
endpoint = "https://dnsapi.cn"
|
|
18
18
|
content_type = TYPE_FORM
|
|
19
19
|
|
|
20
20
|
DefaultLine = "默认"
|
|
@@ -36,9 +36,8 @@ class DnspodProvider(BaseProvider):
|
|
|
36
36
|
if extra:
|
|
37
37
|
params.update(extra)
|
|
38
38
|
params = {k: v for k, v in params.items() if v is not None}
|
|
39
|
-
params.update({"login_token": "{0},{1}".format(self.
|
|
40
|
-
|
|
41
|
-
data = self._http("POST", "/" + action, headers=headers, body=params)
|
|
39
|
+
params.update({"login_token": "{0},{1}".format(self.id, self.token), "format": "json"})
|
|
40
|
+
data = self._http("POST", "/" + action, body=params)
|
|
42
41
|
if data and data.get("status", {}).get("code") == "1": # 请求成功
|
|
43
42
|
return data
|
|
44
43
|
else: # 请求失败
|
|
@@ -102,9 +101,7 @@ class DnspodProvider(BaseProvider):
|
|
|
102
101
|
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
103
102
|
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
104
103
|
"""查询记录 list 然后逐个查找 https://docs.dnspod.cn/api/record-list/"""
|
|
105
|
-
res = self._request(
|
|
106
|
-
"Record.List", domain_id=zone_id, sub_domain=subdomain, record_type=record_type, line=line
|
|
107
|
-
)
|
|
104
|
+
res = self._request("Record.List", domain_id=zone_id, sub_domain=subdomain, record_type=record_type, line=line)
|
|
108
105
|
# length="3000"
|
|
109
106
|
records = res.get("records", [])
|
|
110
107
|
n = len(records)
|
ddns/provider/dnspod_com.py
CHANGED
ddns/provider/edgeone.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
Tencent Cloud EdgeOne API
|
|
4
|
+
腾讯云 EdgeOne (边缘安全速平台) API
|
|
5
|
+
API Documentation: https://cloud.tencent.com/document/api/1552/80731
|
|
6
|
+
@author: NewFuture
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ddns.provider._base import join_domain
|
|
10
|
+
from .tencentcloud import TencentCloudProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EdgeOneProvider(TencentCloudProvider):
|
|
14
|
+
"""
|
|
15
|
+
腾讯云 EdgeOne API 提供商
|
|
16
|
+
Tencent Cloud EdgeOne API Provider
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
endpoint = "https://teo.tencentcloudapi.com"
|
|
20
|
+
# 腾讯云 EdgeOne API 配置
|
|
21
|
+
service = "teo"
|
|
22
|
+
version_date = "2022-09-01"
|
|
23
|
+
|
|
24
|
+
def _query_zone_id(self, domain):
|
|
25
|
+
# type: (str) -> str | None
|
|
26
|
+
"""查询域名的加速域名信息获取 ZoneId https://cloud.tencent.com/document/api/1552/80713"""
|
|
27
|
+
# 首先尝试直接查找域名
|
|
28
|
+
filters = [{"Name": "zone-name", "Values": [domain], "Fuzzy": False}] # type: Any
|
|
29
|
+
response = self._request("DescribeZones", Filters=filters)
|
|
30
|
+
|
|
31
|
+
if response and "Zones" in response:
|
|
32
|
+
for zone in response.get("Zones", []):
|
|
33
|
+
if zone.get("ZoneName") == domain:
|
|
34
|
+
zone_id = zone.get("ZoneId")
|
|
35
|
+
if zone_id:
|
|
36
|
+
self.logger.debug("Found acceleration domain %s with Zone ID: %s", domain, zone_id)
|
|
37
|
+
return zone_id
|
|
38
|
+
|
|
39
|
+
self.logger.debug("Acceleration domain not found for: %s", domain)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
43
|
+
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
44
|
+
"""查询加速域名信息 https://cloud.tencent.com/document/api/1552/86336"""
|
|
45
|
+
domain = join_domain(subdomain, main_domain)
|
|
46
|
+
filters = [{"Name": "domain-name", "Values": [domain], "Fuzzy": False}] # type: Any
|
|
47
|
+
response = self._request("DescribeAccelerationDomains", ZoneId=zone_id, Filters=filters)
|
|
48
|
+
|
|
49
|
+
if response and "AccelerationDomains" in response:
|
|
50
|
+
for domain_info in response.get("AccelerationDomains", []):
|
|
51
|
+
if domain_info.get("DomainName") == domain:
|
|
52
|
+
self.logger.debug("Found acceleration domain: %s", domain_info)
|
|
53
|
+
return domain_info
|
|
54
|
+
|
|
55
|
+
self.logger.warning("No acceleration domain found for: %s, response: %s", domain, response)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
59
|
+
# type: (str, str, str, str, str, int, str | None, dict) -> bool
|
|
60
|
+
"""创建新的加速域名记录 https://cloud.tencent.com/document/api/1552/86338"""
|
|
61
|
+
domain = join_domain(subdomain, main_domain)
|
|
62
|
+
origin = {"OriginType": "IP_DOMAIN", "Origin": value} # type: Any
|
|
63
|
+
res = self._request("CreateAccelerationDomain", ZoneId=zone_id, DomainName=domain, OriginInfo=origin, **extra)
|
|
64
|
+
if res:
|
|
65
|
+
self.logger.info("Acceleration domain created (%s)", res.get("RequestId"))
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
self.logger.error("Failed to create acceleration domain, response: %s", res)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
72
|
+
"""更新加速域名的源站 IP 地址 https://cloud.tencent.com/document/api/1552/86335"""
|
|
73
|
+
domain = old_record.get("DomainName")
|
|
74
|
+
# 构建源站信息
|
|
75
|
+
backup = old_record.get("OriginDetail", {}).get("BackupOrigin", "")
|
|
76
|
+
origin = {"OriginType": "IP_DOMAIN", "Origin": value, "BackupOrigin": backup} # type: Any
|
|
77
|
+
response = self._request("ModifyAccelerationDomain", ZoneId=zone_id, DomainName=domain, OriginInfo=origin)
|
|
78
|
+
|
|
79
|
+
if response:
|
|
80
|
+
self.logger.info("Acceleration domain updated (%s)", response.get("RequestId"))
|
|
81
|
+
return True
|
|
82
|
+
self.logger.error("Failed to update acceleration domain origin, response: %s", response)
|
|
83
|
+
return False
|
ddns/provider/he.py
CHANGED
|
@@ -8,7 +8,7 @@ from ._base import SimpleProvider, TYPE_FORM
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class HeProvider(SimpleProvider):
|
|
11
|
-
|
|
11
|
+
endpoint = "https://dyn.dns.he.net"
|
|
12
12
|
content_type = TYPE_FORM
|
|
13
13
|
accept = None # he.net does not require a specific Accept header
|
|
14
14
|
decode_response = False # he.net response is plain text, not JSON
|
|
@@ -18,9 +18,9 @@ class HeProvider(SimpleProvider):
|
|
|
18
18
|
"HE.net 缺少充分的真实环境测试,请及时在 GitHub Issues 中反馈: %s",
|
|
19
19
|
"https://github.com/NewFuture/DDNS/issues",
|
|
20
20
|
)
|
|
21
|
-
if self.
|
|
21
|
+
if self.id:
|
|
22
22
|
raise ValueError("Hurricane Electric (he.net) does not use `id`, use `token(password)` only.")
|
|
23
|
-
if not self.
|
|
23
|
+
if not self.token:
|
|
24
24
|
raise ValueError("Hurricane Electric (he.net) requires `token(password)`.")
|
|
25
25
|
|
|
26
26
|
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
|
|
@@ -32,7 +32,7 @@ class HeProvider(SimpleProvider):
|
|
|
32
32
|
params = {
|
|
33
33
|
"hostname": domain, # he.net requires full domain name
|
|
34
34
|
"myip": value, # IP address to update
|
|
35
|
-
"password": self.
|
|
35
|
+
"password": self.token, # Use token as password
|
|
36
36
|
}
|
|
37
37
|
try:
|
|
38
38
|
res = self._http("POST", "/nic/update", body=params)
|
ddns/provider/huaweidns.py
CHANGED
|
@@ -5,13 +5,14 @@ HuaweiDNS API
|
|
|
5
5
|
@author: NewFuture
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from
|
|
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
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class HuaweiDNSProvider(BaseProvider):
|
|
14
|
-
|
|
15
|
+
endpoint = "https://dns.myhuaweicloud.com"
|
|
15
16
|
content_type = TYPE_JSON
|
|
16
17
|
algorithm = "SDK-HMAC-SHA256"
|
|
17
18
|
|
|
@@ -32,16 +33,16 @@ class HuaweiDNSProvider(BaseProvider):
|
|
|
32
33
|
# type: (str, str, **Any) -> dict
|
|
33
34
|
params = {k: v for k, v in params.items() if v is not None}
|
|
34
35
|
if method.upper() == "GET" or method.upper() == "DELETE":
|
|
35
|
-
query =
|
|
36
|
+
query = encode_params(params)
|
|
36
37
|
body = ""
|
|
37
38
|
else:
|
|
38
39
|
query = ""
|
|
39
|
-
body =
|
|
40
|
+
body = self._encode_body(params)
|
|
40
41
|
|
|
41
42
|
now = strftime("%Y%m%dT%H%M%SZ", gmtime())
|
|
42
43
|
headers = {
|
|
43
44
|
"content-type": self.content_type,
|
|
44
|
-
"host": self.
|
|
45
|
+
"host": self.endpoint.split("://", 1)[1].strip("/"),
|
|
45
46
|
"X-Sdk-Date": now,
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -51,10 +52,10 @@ class HuaweiDNSProvider(BaseProvider):
|
|
|
51
52
|
sign_path = path if path.endswith("/") else path + "/"
|
|
52
53
|
authorization_format = "%s Access=%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
|
|
53
54
|
self.algorithm,
|
|
54
|
-
self.
|
|
55
|
+
self.id,
|
|
55
56
|
)
|
|
56
57
|
authorization = hmac_sha256_authorization(
|
|
57
|
-
secret_key=self.
|
|
58
|
+
secret_key=self.token,
|
|
58
59
|
method=method,
|
|
59
60
|
path=sign_path,
|
|
60
61
|
query=query,
|
|
@@ -114,7 +115,7 @@ class HuaweiDNSProvider(BaseProvider):
|
|
|
114
115
|
ttl=ttl,
|
|
115
116
|
line=line,
|
|
116
117
|
**extra
|
|
117
|
-
)
|
|
118
|
+
) # fmt: skip
|
|
118
119
|
if res and res.get("id"):
|
|
119
120
|
self.logger.info("Record created: %s", res)
|
|
120
121
|
return True
|
|
@@ -134,7 +135,7 @@ class HuaweiDNSProvider(BaseProvider):
|
|
|
134
135
|
records=[value],
|
|
135
136
|
ttl=ttl if ttl is not None else old_record.get("ttl"),
|
|
136
137
|
**extra
|
|
137
|
-
)
|
|
138
|
+
) # fmt: skip
|
|
138
139
|
if res and res.get("id"):
|
|
139
140
|
self.logger.info("Record updated: %s", res)
|
|
140
141
|
return True
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
NameSilo API Provider
|
|
4
|
+
DNS provider implementation for NameSilo domain registrar
|
|
5
|
+
@doc: https://www.namesilo.com/api-reference
|
|
6
|
+
@author: NewFuture & Copilot
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._base import BaseProvider, TYPE_JSON
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NamesiloProvider(BaseProvider):
|
|
13
|
+
"""
|
|
14
|
+
NameSilo DNS API Provider
|
|
15
|
+
|
|
16
|
+
Supports DNS record management through NameSilo's API including:
|
|
17
|
+
- Domain information retrieval
|
|
18
|
+
- DNS record listing
|
|
19
|
+
- DNS record creation
|
|
20
|
+
- DNS record updating
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
endpoint = "https://www.namesilo.com"
|
|
24
|
+
content_type = TYPE_JSON
|
|
25
|
+
|
|
26
|
+
def _validate(self):
|
|
27
|
+
"""Validate authentication credentials"""
|
|
28
|
+
# NameSilo only requires API key (token), not ID
|
|
29
|
+
if not self.token:
|
|
30
|
+
raise ValueError("API key (token) must be configured for NameSilo")
|
|
31
|
+
if not self.endpoint:
|
|
32
|
+
raise ValueError("API endpoint must be defined in {}".format(self.__class__.__name__))
|
|
33
|
+
|
|
34
|
+
# Warn if ID is configured since NameSilo doesn't need it
|
|
35
|
+
if self.id:
|
|
36
|
+
self.logger.warning("NameSilo does not require 'id' configuration - only API key (token) is needed")
|
|
37
|
+
|
|
38
|
+
# Show pending verification warning
|
|
39
|
+
self.logger.warning("NameSilo provider implementation is pending verification - please test thoroughly")
|
|
40
|
+
|
|
41
|
+
def _request(self, operation, **params):
|
|
42
|
+
# type: (str, **(str | int | bytes | bool | None)) -> dict|None
|
|
43
|
+
"""
|
|
44
|
+
Send request to NameSilo API
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
operation (str): API operation name
|
|
48
|
+
params: API parameters
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
dict: API response data
|
|
52
|
+
"""
|
|
53
|
+
# Filter out None parameters
|
|
54
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
55
|
+
|
|
56
|
+
# Add required authentication and format parameters
|
|
57
|
+
params.update({"version": "1", "type": "json", "key": self.token})
|
|
58
|
+
|
|
59
|
+
# Make API request
|
|
60
|
+
response = self._http("GET", "/api/" + operation, queries=params)
|
|
61
|
+
|
|
62
|
+
# Parse response
|
|
63
|
+
if response and isinstance(response, dict):
|
|
64
|
+
reply = response.get("reply", {})
|
|
65
|
+
|
|
66
|
+
# Check for successful response
|
|
67
|
+
if reply.get("code") == "300": # NameSilo success code
|
|
68
|
+
return reply
|
|
69
|
+
else:
|
|
70
|
+
# Log error details
|
|
71
|
+
error_msg = reply.get("detail", "Unknown error")
|
|
72
|
+
self.logger.warning("NameSilo API error [%s]: %s", reply.get("code", "unknown"), error_msg)
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def _query_zone_id(self, domain):
|
|
77
|
+
# type: (str) -> str | None
|
|
78
|
+
"""
|
|
79
|
+
Query domain information to get domain as zone identifier
|
|
80
|
+
@doc: https://www.namesilo.com/api-reference#domains/get-domain-info
|
|
81
|
+
"""
|
|
82
|
+
response = self._request("getDomainInfo", domain=domain)
|
|
83
|
+
|
|
84
|
+
if response:
|
|
85
|
+
# Domain exists, return the domain name as zone_id
|
|
86
|
+
domain_info = response.get("domain", {})
|
|
87
|
+
if domain_info:
|
|
88
|
+
self.logger.debug("Domain found: %s", domain)
|
|
89
|
+
return domain
|
|
90
|
+
|
|
91
|
+
self.logger.warning("Domain not found or not accessible: %s", domain)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
95
|
+
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
96
|
+
"""
|
|
97
|
+
Query existing DNS record
|
|
98
|
+
@doc: https://www.namesilo.com/api-reference#dns/list-dns-records
|
|
99
|
+
"""
|
|
100
|
+
response = self._request("dnsListRecords", domain=main_domain)
|
|
101
|
+
|
|
102
|
+
if response:
|
|
103
|
+
records = response.get("resource_record", [])
|
|
104
|
+
|
|
105
|
+
# Find matching record
|
|
106
|
+
for record in records:
|
|
107
|
+
if record.get("host") == subdomain and record.get("type") == record_type:
|
|
108
|
+
self.logger.debug("Found existing record: %s", record)
|
|
109
|
+
return record
|
|
110
|
+
|
|
111
|
+
self.logger.debug("No matching record found for %s.%s (%s)", subdomain, main_domain, record_type)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
115
|
+
# type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
|
|
116
|
+
"""
|
|
117
|
+
Create new DNS record
|
|
118
|
+
@doc: https://www.namesilo.com/api-reference#dns/add-dns-record
|
|
119
|
+
"""
|
|
120
|
+
response = self._request(
|
|
121
|
+
"dnsAddRecord", domain=main_domain, rrtype=record_type, rrhost=subdomain, rrvalue=value, rrttl=ttl
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if response:
|
|
125
|
+
record_id = response.get("record_id")
|
|
126
|
+
self.logger.info("DNS record created successfully: %s", record_id)
|
|
127
|
+
return True
|
|
128
|
+
else:
|
|
129
|
+
self.logger.error("Failed to create DNS record")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
133
|
+
# type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
|
|
134
|
+
"""
|
|
135
|
+
Update existing DNS record
|
|
136
|
+
@doc: https://www.namesilo.com/api-reference#dns/update-dns-record
|
|
137
|
+
"""
|
|
138
|
+
record_id = old_record.get("record_id")
|
|
139
|
+
if not record_id:
|
|
140
|
+
self.logger.error("No record_id found in old_record: %s", old_record)
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
# In NameSilo, zone_id is the main domain name
|
|
144
|
+
response = self._request(
|
|
145
|
+
"dnsUpdateRecord",
|
|
146
|
+
rrid=record_id,
|
|
147
|
+
domain=zone_id, # zone_id is main_domain in NameSilo
|
|
148
|
+
rrhost=old_record.get("host"), # host field contains subdomain
|
|
149
|
+
rrvalue=value,
|
|
150
|
+
rrtype=record_type,
|
|
151
|
+
rrttl=ttl or old_record.get("ttl"),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if response:
|
|
155
|
+
self.logger.info("DNS record updated successfully: %s", record_id)
|
|
156
|
+
return True
|
|
157
|
+
else:
|
|
158
|
+
self.logger.error("Failed to update DNS record")
|
|
159
|
+
return False
|