ddns 4.1.0b1__py2.py3-none-any.whl → 4.1.0b2__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.
- ddns/__init__.py +2 -11
- ddns/__main__.py +67 -142
- ddns/{util/cache.py → cache.py} +42 -11
- ddns/{util/ip.py → ip.py} +11 -9
- ddns/provider/__init__.py +24 -4
- ddns/provider/_base.py +78 -198
- ddns/provider/_signature.py +113 -0
- ddns/provider/alidns.py +31 -14
- ddns/provider/aliesa.py +129 -0
- ddns/provider/callback.py +9 -10
- ddns/provider/cloudflare.py +8 -8
- ddns/provider/dnscom.py +5 -5
- ddns/provider/dnspod.py +3 -4
- ddns/provider/dnspod_com.py +1 -1
- ddns/provider/edgeone.py +82 -0
- ddns/provider/he.py +4 -4
- ddns/provider/huaweidns.py +8 -8
- ddns/provider/namesilo.py +159 -0
- ddns/provider/noip.py +103 -0
- ddns/provider/tencentcloud.py +7 -7
- ddns/util/comment.py +88 -0
- ddns/util/http.py +85 -134
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b2.dist-info}/METADATA +22 -13
- ddns-4.1.0b2.dist-info/RECORD +31 -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.0b2.dist-info}/WHEEL +0 -0
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b2.dist-info}/entry_points.txt +0 -0
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b2.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.1.0b1.dist-info → ddns-4.1.0b2.dist-info}/top_level.txt +0 -0
ddns/provider/aliesa.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
AliESA API
|
|
4
|
+
阿里云边缘安全加速(ESA) DNS 解析操作库
|
|
5
|
+
@author: NewFuture, GitHub Copilot
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .alidns import AliBaseProvider
|
|
9
|
+
from ._base import join_domain, TYPE_JSON
|
|
10
|
+
from time import strftime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AliesaProvider(AliBaseProvider):
|
|
14
|
+
"""阿里云边缘安全加速(ESA) DNS Provider"""
|
|
15
|
+
|
|
16
|
+
endpoint = "https://esa.cn-hangzhou.aliyuncs.com"
|
|
17
|
+
api_version = "2024-09-10" # ESA API版本
|
|
18
|
+
content_type = TYPE_JSON
|
|
19
|
+
remark = "Managed by DDNS %s" % strftime("%Y-%m-%d %H:%M:%S")
|
|
20
|
+
|
|
21
|
+
def _query_zone_id(self, domain):
|
|
22
|
+
# type: (str) -> str | None
|
|
23
|
+
"""
|
|
24
|
+
查询站点ID
|
|
25
|
+
https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listsites
|
|
26
|
+
"""
|
|
27
|
+
res = self._request(method="GET", action="ListSites", SiteName=domain, PageSize=500)
|
|
28
|
+
sites = res.get("Sites", [])
|
|
29
|
+
|
|
30
|
+
for site in sites:
|
|
31
|
+
if site.get("SiteName") == domain:
|
|
32
|
+
site_id = site.get("SiteId")
|
|
33
|
+
self.logger.debug("Found site ID %s for domain %s", site_id, domain)
|
|
34
|
+
return site_id
|
|
35
|
+
|
|
36
|
+
self.logger.error("Site not found for domain: %s", domain)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
40
|
+
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
41
|
+
"""
|
|
42
|
+
查询DNS记录
|
|
43
|
+
https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listrecords
|
|
44
|
+
"""
|
|
45
|
+
full_domain = join_domain(subdomain, main_domain)
|
|
46
|
+
res = self._request(
|
|
47
|
+
method="GET",
|
|
48
|
+
action="ListRecords",
|
|
49
|
+
SiteId=int(zone_id),
|
|
50
|
+
RecordName=full_domain,
|
|
51
|
+
Type=self._get_type(record_type),
|
|
52
|
+
RecordMatchType="exact", # 精确匹配
|
|
53
|
+
PageSize=100,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
records = res.get("Records", [])
|
|
57
|
+
if len(records) == 0:
|
|
58
|
+
self.logger.warning("No records found for [%s] with %s <%s>", zone_id, full_domain, record_type)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# 返回第一个匹配的记录
|
|
62
|
+
record = records[0]
|
|
63
|
+
self.logger.debug("Found record: %s", record)
|
|
64
|
+
return record
|
|
65
|
+
|
|
66
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
67
|
+
# type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
|
|
68
|
+
"""
|
|
69
|
+
创建DNS记录
|
|
70
|
+
https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord
|
|
71
|
+
"""
|
|
72
|
+
full_domain = join_domain(subdomain, main_domain)
|
|
73
|
+
extra["Comment"] = extra.get("Comment", self.remark)
|
|
74
|
+
extra["BizName"] = extra.get("BizName", "web")
|
|
75
|
+
extra["Proxied"] = extra.get("Proxied", True)
|
|
76
|
+
data = self._request(
|
|
77
|
+
method="POST",
|
|
78
|
+
action="CreateRecord",
|
|
79
|
+
SiteId=int(zone_id),
|
|
80
|
+
RecordName=full_domain,
|
|
81
|
+
Type=self._get_type(record_type),
|
|
82
|
+
Data={"Value": value},
|
|
83
|
+
Ttl=ttl or 1,
|
|
84
|
+
**extra
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if data and data.get("RecordId"):
|
|
88
|
+
self.logger.info("Record created: %s", data)
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
self.logger.error("Failed to create record: %s", data)
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
95
|
+
# type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
|
|
96
|
+
"""
|
|
97
|
+
更新DNS记录
|
|
98
|
+
https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updaterecord
|
|
99
|
+
"""
|
|
100
|
+
# 检查是否需要更新
|
|
101
|
+
if (
|
|
102
|
+
old_record.get("Data", {}).get("Value") == value
|
|
103
|
+
and old_record.get("RecordType") == self._get_type(record_type)
|
|
104
|
+
and (not ttl or old_record.get("Ttl") == ttl)
|
|
105
|
+
):
|
|
106
|
+
self.logger.warning("No changes detected, skipping update for record: %s", old_record.get("RecordName"))
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
extra["Comment"] = extra.get("Comment", self.remark)
|
|
110
|
+
extra["Proxied"] = extra.get("Proxied", old_record.get("Proxied"))
|
|
111
|
+
data = self._request(
|
|
112
|
+
method="POST",
|
|
113
|
+
action="UpdateRecord",
|
|
114
|
+
RecordId=old_record.get("RecordId"),
|
|
115
|
+
Data={"Value": value},
|
|
116
|
+
Ttl=ttl,
|
|
117
|
+
**extra
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if data and data.get("RecordId"):
|
|
121
|
+
self.logger.info("Record updated: %s", data)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
self.logger.error("Failed to update record: %s", data)
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def _get_type(self, record_type):
|
|
128
|
+
# type: (str) -> str
|
|
129
|
+
return "A/AAAA" if record_type in ("A", "AAAA") else record_type
|
ddns/provider/callback.py
CHANGED
|
@@ -16,7 +16,7 @@ class CallbackProvider(SimpleProvider):
|
|
|
16
16
|
Generic custom callback provider, supports GET/POST arbitrary API.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
endpoint = "" # CallbackProvider uses id as URL, no fixed API endpoint
|
|
20
20
|
content_type = TYPE_JSON
|
|
21
21
|
decode_response = False # Callback response is not JSON, it's a custom response
|
|
22
22
|
|
|
@@ -26,9 +26,8 @@ class CallbackProvider(SimpleProvider):
|
|
|
26
26
|
Send custom callback request, support GET/POST
|
|
27
27
|
"""
|
|
28
28
|
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)}
|
|
29
|
+
url = self.id # 直接用 id 作为 url
|
|
30
|
+
token = self.token # token 作为 POST 参数
|
|
32
31
|
extra.update(
|
|
33
32
|
{
|
|
34
33
|
"__DOMAIN__": domain,
|
|
@@ -51,7 +50,7 @@ class CallbackProvider(SimpleProvider):
|
|
|
51
50
|
params[k] = self._replace_vars(v, extra)
|
|
52
51
|
|
|
53
52
|
try:
|
|
54
|
-
res = self._http(method, url, body=params
|
|
53
|
+
res = self._http(method, url, body=params)
|
|
55
54
|
if res is not None:
|
|
56
55
|
self.logger.info("Callback result: %s", res)
|
|
57
56
|
return True
|
|
@@ -72,8 +71,8 @@ class CallbackProvider(SimpleProvider):
|
|
|
72
71
|
return string
|
|
73
72
|
|
|
74
73
|
def _validate(self):
|
|
75
|
-
# CallbackProvider uses
|
|
76
|
-
if not self.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
# CallbackProvider uses id as URL, not as regular ID
|
|
75
|
+
if self.endpoint or (not self.id or "://" not in self.id):
|
|
76
|
+
# 如果 endpoint 已经设置,或者 id 不是有效的 URL,则抛出异常
|
|
77
|
+
self.logger.critical("endpoint [%s] or id [%s] 必须是有效的URL", self.endpoint, self.id)
|
|
78
|
+
raise ValueError("endpoint or id must be configured with URL")
|
ddns/provider/cloudflare.py
CHANGED
|
@@ -8,26 +8,26 @@ from ._base import BaseProvider, TYPE_JSON, 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)
|
ddns/provider/dnscom.py
CHANGED
|
@@ -5,7 +5,7 @@ www.51dns.com (原dns.com)
|
|
|
5
5
|
@author: Bigjin<i@bigjin.com>, NewFuture
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from ._base import BaseProvider, TYPE_FORM
|
|
8
|
+
from ._base import BaseProvider, TYPE_FORM, encode_params
|
|
9
9
|
from hashlib import md5
|
|
10
10
|
from time import time
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ class DnscomProvider(BaseProvider):
|
|
|
16
16
|
https://www.51dns.com/document/api/index.html
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
endpoint = "https://www.51dns.com"
|
|
20
20
|
content_type = TYPE_FORM
|
|
21
21
|
|
|
22
22
|
def _validate(self):
|
|
@@ -31,12 +31,12 @@ class DnscomProvider(BaseProvider):
|
|
|
31
31
|
params = {k: v for k, v in params.items() if v is not None}
|
|
32
32
|
params.update(
|
|
33
33
|
{
|
|
34
|
-
"apiKey": self.
|
|
34
|
+
"apiKey": self.id,
|
|
35
35
|
"timestamp": time(), # 时间戳
|
|
36
36
|
}
|
|
37
37
|
)
|
|
38
|
-
query =
|
|
39
|
-
sign = md5((query + self.
|
|
38
|
+
query = encode_params(params)
|
|
39
|
+
sign = md5((query + self.token).encode("utf-8")).hexdigest()
|
|
40
40
|
params["hash"] = sign
|
|
41
41
|
return params
|
|
42
42
|
|
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: # 请求失败
|
ddns/provider/dnspod_com.py
CHANGED
ddns/provider/edgeone.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
from ddns.provider._base import join_domain
|
|
9
|
+
from .tencentcloud import TencentCloudProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EdgeOneProvider(TencentCloudProvider):
|
|
13
|
+
"""
|
|
14
|
+
腾讯云 EdgeOne API 提供商
|
|
15
|
+
Tencent Cloud EdgeOne API Provider
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
endpoint = "https://teo.tencentcloudapi.com"
|
|
19
|
+
# 腾讯云 EdgeOne API 配置
|
|
20
|
+
service = "teo"
|
|
21
|
+
version_date = "2022-09-01"
|
|
22
|
+
|
|
23
|
+
def _query_zone_id(self, domain):
|
|
24
|
+
# type: (str) -> str | None
|
|
25
|
+
"""查询域名的加速域名信息获取 ZoneId https://cloud.tencent.com/document/api/1552/80713"""
|
|
26
|
+
# 首先尝试直接查找域名
|
|
27
|
+
filters = [{"Name": "zone-name", "Values": [domain], "Fuzzy": False}] # type: Any
|
|
28
|
+
response = self._request("DescribeZones", Filters=filters)
|
|
29
|
+
|
|
30
|
+
if response and "Zones" in response:
|
|
31
|
+
for zone in response.get("Zones", []):
|
|
32
|
+
if zone.get("ZoneName") == domain:
|
|
33
|
+
zone_id = zone.get("ZoneId")
|
|
34
|
+
if zone_id:
|
|
35
|
+
self.logger.debug("Found acceleration domain %s with Zone ID: %s", domain, zone_id)
|
|
36
|
+
return zone_id
|
|
37
|
+
|
|
38
|
+
self.logger.debug("Acceleration domain not found for: %s", domain)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
42
|
+
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
43
|
+
"""查询加速域名信息 https://cloud.tencent.com/document/api/1552/86336"""
|
|
44
|
+
domain = join_domain(subdomain, main_domain)
|
|
45
|
+
filters = [{"Name": "domain-name", "Values": [domain], "Fuzzy": False}] # type: Any
|
|
46
|
+
response = self._request("DescribeAccelerationDomains", ZoneId=zone_id, Filters=filters)
|
|
47
|
+
|
|
48
|
+
if response and "AccelerationDomains" in response:
|
|
49
|
+
for domain_info in response.get("AccelerationDomains", []):
|
|
50
|
+
if domain_info.get("DomainName") == domain:
|
|
51
|
+
self.logger.debug("Found acceleration domain: %s", domain_info)
|
|
52
|
+
return domain_info
|
|
53
|
+
|
|
54
|
+
self.logger.warning("No acceleration domain found for: %s, response: %s", domain, response)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
58
|
+
# type: (str, str, str, str, str, int, str | None, dict) -> bool
|
|
59
|
+
"""创建新的加速域名记录 https://cloud.tencent.com/document/api/1552/86338"""
|
|
60
|
+
domain = join_domain(subdomain, main_domain)
|
|
61
|
+
origin = {"OriginType": "IP_DOMAIN", "Origin": value} # type: Any
|
|
62
|
+
res = self._request("CreateAccelerationDomain", ZoneId=zone_id, DomainName=domain, OriginInfo=origin, **extra)
|
|
63
|
+
if res:
|
|
64
|
+
self.logger.info("Acceleration domain created (%s)", res.get("RequestId"))
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
self.logger.error("Failed to create acceleration domain, response: %s", res)
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
71
|
+
"""更新加速域名的源站 IP 地址 https://cloud.tencent.com/document/api/1552/86335"""
|
|
72
|
+
domain = old_record.get("DomainName")
|
|
73
|
+
# 构建源站信息
|
|
74
|
+
backup = old_record.get("OriginDetail", {}).get("BackupOrigin", "")
|
|
75
|
+
origin = {"OriginType": "IP_DOMAIN", "Origin": value, "BackupOrigin": backup} # type: Any
|
|
76
|
+
response = self._request("ModifyAccelerationDomain", ZoneId=zone_id, DomainName=domain, OriginInfo=origin)
|
|
77
|
+
|
|
78
|
+
if response:
|
|
79
|
+
self.logger.info("Acceleration domain updated (%s)", response.get("RequestId"))
|
|
80
|
+
return True
|
|
81
|
+
self.logger.error("Failed to update acceleration domain origin, response: %s", response)
|
|
82
|
+
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,13 @@ HuaweiDNS API
|
|
|
5
5
|
@author: NewFuture
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from ._base import BaseProvider, TYPE_JSON,
|
|
9
|
-
from
|
|
8
|
+
from ._base import BaseProvider, TYPE_JSON, join_domain, encode_params
|
|
9
|
+
from ._signature import hmac_sha256_authorization, sha256_hash
|
|
10
10
|
from time import strftime, gmtime
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class HuaweiDNSProvider(BaseProvider):
|
|
14
|
-
|
|
14
|
+
endpoint = "https://dns.myhuaweicloud.com"
|
|
15
15
|
content_type = TYPE_JSON
|
|
16
16
|
algorithm = "SDK-HMAC-SHA256"
|
|
17
17
|
|
|
@@ -32,16 +32,16 @@ class HuaweiDNSProvider(BaseProvider):
|
|
|
32
32
|
# type: (str, str, **Any) -> dict
|
|
33
33
|
params = {k: v for k, v in params.items() if v is not None}
|
|
34
34
|
if method.upper() == "GET" or method.upper() == "DELETE":
|
|
35
|
-
query =
|
|
35
|
+
query = encode_params(params)
|
|
36
36
|
body = ""
|
|
37
37
|
else:
|
|
38
38
|
query = ""
|
|
39
|
-
body =
|
|
39
|
+
body = self._encode_body(params)
|
|
40
40
|
|
|
41
41
|
now = strftime("%Y%m%dT%H%M%SZ", gmtime())
|
|
42
42
|
headers = {
|
|
43
43
|
"content-type": self.content_type,
|
|
44
|
-
"host": self.
|
|
44
|
+
"host": self.endpoint.split("://", 1)[1].strip("/"),
|
|
45
45
|
"X-Sdk-Date": now,
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -51,10 +51,10 @@ class HuaweiDNSProvider(BaseProvider):
|
|
|
51
51
|
sign_path = path if path.endswith("/") else path + "/"
|
|
52
52
|
authorization_format = "%s Access=%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
|
|
53
53
|
self.algorithm,
|
|
54
|
-
self.
|
|
54
|
+
self.id,
|
|
55
55
|
)
|
|
56
56
|
authorization = hmac_sha256_authorization(
|
|
57
|
-
secret_key=self.
|
|
57
|
+
secret_key=self.token,
|
|
58
58
|
method=method,
|
|
59
59
|
path=sign_path,
|
|
60
60
|
query=query,
|
|
@@ -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
|