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,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
|
ddns/provider/noip.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
No-IP (noip.com) Dynamic DNS API
|
|
4
|
+
@author: GitHub Copilot
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._base import SimpleProvider, TYPE_FORM, quote
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NoipProvider(SimpleProvider):
|
|
11
|
+
"""
|
|
12
|
+
No-IP (www.noip.com) Dynamic DNS Provider
|
|
13
|
+
|
|
14
|
+
No-IP is a popular dynamic DNS service that provides simple HTTP-based
|
|
15
|
+
API for updating DNS records. This provider supports the standard
|
|
16
|
+
No-IP update protocol.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
endpoint = "https://dynupdate.no-ip.com"
|
|
20
|
+
content_type = TYPE_FORM
|
|
21
|
+
accept = None # No-IP returns plain text response
|
|
22
|
+
decode_response = False # Response is plain text, not JSON
|
|
23
|
+
|
|
24
|
+
def _validate(self):
|
|
25
|
+
"""
|
|
26
|
+
Validate authentication credentials for No-IP and update endpoint with auth
|
|
27
|
+
"""
|
|
28
|
+
# Check endpoint first
|
|
29
|
+
if not self.endpoint or "://" not in self.endpoint:
|
|
30
|
+
raise ValueError("API endpoint must be defined and contain protocol")
|
|
31
|
+
|
|
32
|
+
if not self.id:
|
|
33
|
+
raise ValueError("No-IP requires username as 'id'")
|
|
34
|
+
if not self.token:
|
|
35
|
+
raise ValueError("No-IP requires password as 'token'")
|
|
36
|
+
|
|
37
|
+
# Update endpoint with URL-encoded auth credentials
|
|
38
|
+
protocol, domain = self.endpoint.split("://", 1)
|
|
39
|
+
self.endpoint = "{0}://{1}:{2}@{3}".format(
|
|
40
|
+
protocol, quote(self.id, safe=""), quote(self.token, safe=""), domain
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
|
|
44
|
+
"""
|
|
45
|
+
Update DNS record using No-IP Dynamic Update API
|
|
46
|
+
|
|
47
|
+
No-IP API Reference:
|
|
48
|
+
- URL: https://dynupdate.no-ip.com/nic/update
|
|
49
|
+
- Method: GET or POST
|
|
50
|
+
- Authentication: HTTP Basic Auth (username:password)
|
|
51
|
+
- Parameters:
|
|
52
|
+
- hostname: The hostname to update
|
|
53
|
+
- myip: The IP address to set (optional, uses client IP
|
|
54
|
+
if not provided)
|
|
55
|
+
|
|
56
|
+
Response codes:
|
|
57
|
+
- good: Update successful
|
|
58
|
+
- nochg: IP address is current, no update performed
|
|
59
|
+
- nohost: Hostname supplied does not exist
|
|
60
|
+
- badauth: Invalid username/password combination
|
|
61
|
+
- badagent: Client disabled
|
|
62
|
+
- !donator: An update request was sent including a feature that
|
|
63
|
+
is not available
|
|
64
|
+
- abuse: Username is blocked due to abuse
|
|
65
|
+
"""
|
|
66
|
+
self.logger.info("%s => %s(%s)", domain, value, record_type)
|
|
67
|
+
|
|
68
|
+
# Prepare request parameters
|
|
69
|
+
params = {"hostname": domain, "myip": value}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Use GET request as it's the most common method for DDNS
|
|
73
|
+
# Endpoint already includes auth credentials from _validate()
|
|
74
|
+
response = self._http("GET", "/nic/update", queries=params)
|
|
75
|
+
|
|
76
|
+
if response is not None:
|
|
77
|
+
response_str = str(response).strip()
|
|
78
|
+
self.logger.info("No-IP API response: %s", response_str)
|
|
79
|
+
|
|
80
|
+
# Check for successful responses
|
|
81
|
+
if response_str.startswith("good") or response_str.startswith("nochg"):
|
|
82
|
+
return True
|
|
83
|
+
elif response_str.startswith("nohost"):
|
|
84
|
+
self.logger.error("Hostname %s does not exist under No-IP account", domain)
|
|
85
|
+
elif response_str.startswith("badauth"):
|
|
86
|
+
self.logger.error("Invalid No-IP username/password combination")
|
|
87
|
+
elif response_str.startswith("badagent"):
|
|
88
|
+
self.logger.error("No-IP client disabled")
|
|
89
|
+
elif response_str.startswith("!donator"):
|
|
90
|
+
self.logger.error("Feature not available for No-IP free account")
|
|
91
|
+
elif response_str.startswith("abuse"):
|
|
92
|
+
self.logger.error("No-IP account blocked due to abuse")
|
|
93
|
+
else:
|
|
94
|
+
self.logger.error("Unexpected No-IP API response: %s", response_str)
|
|
95
|
+
else:
|
|
96
|
+
self.logger.error("Empty response from No-IP API")
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
self.logger.error("Error updating No-IP record for %s: %s", domain, e)
|
|
100
|
+
|
|
101
|
+
return False
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
Tencent Cloud DNSPod API
|
|
4
|
+
腾讯云 DNSPod API
|
|
5
|
+
|
|
6
|
+
@author: NewFuture
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from time import gmtime, strftime, time
|
|
10
|
+
|
|
11
|
+
from ._base import TYPE_JSON, BaseProvider
|
|
12
|
+
from ._signature import hmac_sha256, hmac_sha256_authorization, sha256_hash
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TencentCloudProvider(BaseProvider):
|
|
16
|
+
"""
|
|
17
|
+
腾讯云 DNSPod API 提供商
|
|
18
|
+
Tencent Cloud DNSPod API Provider
|
|
19
|
+
|
|
20
|
+
API Version: 2021-03-23
|
|
21
|
+
Documentation: https://cloud.tencent.com/document/api/1427
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
endpoint = "https://dnspod.tencentcloudapi.com"
|
|
25
|
+
content_type = TYPE_JSON
|
|
26
|
+
|
|
27
|
+
# 腾讯云 DNSPod API 配置
|
|
28
|
+
service = "dnspod"
|
|
29
|
+
version_date = "2021-03-23"
|
|
30
|
+
|
|
31
|
+
def _request(self, action, **params):
|
|
32
|
+
# type: (str, **(str | int | bytes | bool | None)) -> dict | None
|
|
33
|
+
"""
|
|
34
|
+
发送腾讯云 API 请求
|
|
35
|
+
|
|
36
|
+
API 文档: https://cloud.tencent.com/document/api/1427/56187
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
action (str): API 操作名称
|
|
40
|
+
params (dict): 请求参数
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
dict: API 响应结果
|
|
44
|
+
"""
|
|
45
|
+
# 构建请求体
|
|
46
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
47
|
+
body = self._encode_body(params)
|
|
48
|
+
|
|
49
|
+
# 构建请求头,小写 腾讯云只签名特定头部
|
|
50
|
+
headers = {"content-type": self.content_type, "host": self.endpoint.split("://", 1)[1].strip("/")}
|
|
51
|
+
|
|
52
|
+
# 腾讯云特殊的密钥派生过程
|
|
53
|
+
date = strftime("%Y-%m-%d", gmtime())
|
|
54
|
+
credential_scope = "{}/{}/tc3_request".format(date, self.service)
|
|
55
|
+
|
|
56
|
+
# 派生签名密钥
|
|
57
|
+
secret_date = hmac_sha256("TC3" + self.token, date).digest()
|
|
58
|
+
secret_service = hmac_sha256(secret_date, self.service).digest()
|
|
59
|
+
signing_key = hmac_sha256(secret_service, "tc3_request").digest()
|
|
60
|
+
|
|
61
|
+
# 预处理模板字符串
|
|
62
|
+
auth_format = "TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
|
|
63
|
+
self.id,
|
|
64
|
+
credential_scope,
|
|
65
|
+
)
|
|
66
|
+
timestamp = str(int(time()))
|
|
67
|
+
sign_template = "\n".join(["TC3-HMAC-SHA256", timestamp, credential_scope, "{HashedCanonicalRequest}"])
|
|
68
|
+
authorization = hmac_sha256_authorization(
|
|
69
|
+
secret_key=signing_key,
|
|
70
|
+
method="POST",
|
|
71
|
+
path="/",
|
|
72
|
+
query="",
|
|
73
|
+
headers=headers,
|
|
74
|
+
body_hash=sha256_hash(body),
|
|
75
|
+
signing_string_format=sign_template,
|
|
76
|
+
authorization_format=auth_format,
|
|
77
|
+
)
|
|
78
|
+
# X-TC 更新签名之后方可添加
|
|
79
|
+
headers.update(
|
|
80
|
+
{
|
|
81
|
+
"X-TC-Action": action,
|
|
82
|
+
"X-TC-Version": self.version_date,
|
|
83
|
+
"X-TC-Timestamp": timestamp,
|
|
84
|
+
"authorization": authorization,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
response = self._http("POST", "/", body=body, headers=headers)
|
|
89
|
+
if response and "Response" in response:
|
|
90
|
+
if "Error" in response["Response"]:
|
|
91
|
+
error = response["Response"]["Error"]
|
|
92
|
+
self.logger.error(
|
|
93
|
+
"TencentCloud API error: %s - %s",
|
|
94
|
+
error.get("Code", "Unknown"),
|
|
95
|
+
error.get("Message", "Unknown error"),
|
|
96
|
+
)
|
|
97
|
+
return None
|
|
98
|
+
return response["Response"]
|
|
99
|
+
|
|
100
|
+
self.logger.warning("Unexpected response format: %s", response)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def _query_zone_id(self, domain):
|
|
104
|
+
# type: (str) -> str | None
|
|
105
|
+
"""查询域名的 zone_id (domain id) https://cloud.tencent.com/document/api/1427/56173"""
|
|
106
|
+
# 使用 DescribeDomain API 查询指定域名的信息
|
|
107
|
+
response = self._request("DescribeDomain", Domain=domain)
|
|
108
|
+
|
|
109
|
+
if not response or "DomainInfo" not in response:
|
|
110
|
+
self.logger.debug("Domain info not found or query failed for: %s", domain)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
domain_id = response.get("DomainInfo", {}).get("DomainId")
|
|
114
|
+
|
|
115
|
+
if domain_id is not None:
|
|
116
|
+
self.logger.debug("Found domain %s with ID: %s", domain, domain_id)
|
|
117
|
+
return str(domain_id)
|
|
118
|
+
|
|
119
|
+
self.logger.debug("Domain ID not found in response for: %s", domain)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
123
|
+
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
124
|
+
"""查询 DNS 记录列表 https://cloud.tencent.com/document/api/1427/56166"""
|
|
125
|
+
|
|
126
|
+
response = self._request(
|
|
127
|
+
"DescribeRecordList",
|
|
128
|
+
DomainId=int(zone_id),
|
|
129
|
+
Subdomain=subdomain,
|
|
130
|
+
Domain=main_domain,
|
|
131
|
+
RecordType=record_type,
|
|
132
|
+
RecordLine=line,
|
|
133
|
+
**extra
|
|
134
|
+
) # fmt: skip
|
|
135
|
+
if not response or "RecordList" not in response:
|
|
136
|
+
self.logger.debug("No records found or query failed")
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
records = response["RecordList"]
|
|
140
|
+
if not records:
|
|
141
|
+
self.logger.debug("No records found for subdomain: %s", subdomain)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# 查找匹配的记录
|
|
145
|
+
target_name = subdomain if subdomain and subdomain != "@" else "@"
|
|
146
|
+
for record in records:
|
|
147
|
+
if record.get("Name") == target_name and record.get("Type") == record_type.upper():
|
|
148
|
+
self.logger.debug("Found existing record: %s", record)
|
|
149
|
+
return record
|
|
150
|
+
|
|
151
|
+
self.logger.debug("No matching record found")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
155
|
+
"""创建 DNS 记录 https://cloud.tencent.com/document/api/1427/56180"""
|
|
156
|
+
extra["Remark"] = extra.get("Remark", self.remark)
|
|
157
|
+
response = self._request(
|
|
158
|
+
"CreateRecord",
|
|
159
|
+
Domain=main_domain,
|
|
160
|
+
DomainId=int(zone_id),
|
|
161
|
+
SubDomain=subdomain,
|
|
162
|
+
RecordType=record_type,
|
|
163
|
+
Value=value,
|
|
164
|
+
RecordLine=line or "默认",
|
|
165
|
+
TTL=int(ttl) if ttl else None,
|
|
166
|
+
**extra
|
|
167
|
+
) # fmt: skip
|
|
168
|
+
if response and "RecordId" in response:
|
|
169
|
+
self.logger.info("Record created successfully with ID: %s", response["RecordId"])
|
|
170
|
+
return True
|
|
171
|
+
self.logger.error("Failed to create record:\n%s", response)
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
175
|
+
"""更新 DNS 记录: https://cloud.tencent.com/document/api/1427/56157"""
|
|
176
|
+
extra["Remark"] = extra.get("Remark", self.remark)
|
|
177
|
+
response = self._request(
|
|
178
|
+
"ModifyRecord",
|
|
179
|
+
Domain=old_record.get("Domain", ""),
|
|
180
|
+
DomainId=old_record.get("DomainId", int(zone_id)),
|
|
181
|
+
SubDomain=old_record.get("Name"),
|
|
182
|
+
RecordId=old_record.get("RecordId"),
|
|
183
|
+
RecordType=record_type,
|
|
184
|
+
RecordLine=old_record.get("Line", line or "默认"),
|
|
185
|
+
Value=value,
|
|
186
|
+
TTL=int(ttl) if ttl else None,
|
|
187
|
+
**extra
|
|
188
|
+
) # fmt: skip
|
|
189
|
+
if response and "RecordId" in response:
|
|
190
|
+
self.logger.info("Record updated successfully")
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
self.logger.error("Failed to update record")
|
|
194
|
+
return False
|
ddns/util/comment.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Comment removal utility for JSON configuration files.
|
|
4
|
+
Supports both # and // style single line comments.
|
|
5
|
+
@author: GitHub Copilot
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def remove_comment(content):
|
|
10
|
+
# type: (str) -> str
|
|
11
|
+
"""
|
|
12
|
+
移除字符串中的单行注释。
|
|
13
|
+
支持 # 和 // 两种注释风格。
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
content (str): 包含注释的字符串内容
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: 移除注释后的字符串
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
>>> remove_comment('{"key": "value"} // comment')
|
|
23
|
+
'{"key": "value"} '
|
|
24
|
+
>>> remove_comment('# This is a comment\\n{"key": "value"}')
|
|
25
|
+
'\\n{"key": "value"}'
|
|
26
|
+
"""
|
|
27
|
+
if not content:
|
|
28
|
+
return content
|
|
29
|
+
|
|
30
|
+
lines = content.splitlines()
|
|
31
|
+
cleaned_lines = []
|
|
32
|
+
|
|
33
|
+
for line in lines:
|
|
34
|
+
# 移除行内注释,但要小心不要破坏字符串内的内容
|
|
35
|
+
cleaned_line = _remove_line_comment(line)
|
|
36
|
+
cleaned_lines.append(cleaned_line)
|
|
37
|
+
|
|
38
|
+
return "\n".join(cleaned_lines)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _remove_line_comment(line):
|
|
42
|
+
# type: (str) -> str
|
|
43
|
+
"""
|
|
44
|
+
移除单行中的注释部分。
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
line (str): 要处理的行
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: 移除注释后的行
|
|
51
|
+
"""
|
|
52
|
+
# 检查是否是整行注释
|
|
53
|
+
stripped = line.lstrip()
|
|
54
|
+
if stripped.startswith("#") or stripped.startswith("//"):
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
# 查找行内注释,需要考虑字符串内容
|
|
58
|
+
in_string = False
|
|
59
|
+
quote_char = None
|
|
60
|
+
i = 0
|
|
61
|
+
|
|
62
|
+
while i < len(line):
|
|
63
|
+
char = line[i]
|
|
64
|
+
|
|
65
|
+
# 处理字符串内的转义序列
|
|
66
|
+
if in_string and char == "\\" and i + 1 < len(line):
|
|
67
|
+
i += 2 # 跳过转义字符
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# 处理引号字符
|
|
71
|
+
if char in ('"', "'"):
|
|
72
|
+
if not in_string:
|
|
73
|
+
in_string = True
|
|
74
|
+
quote_char = char
|
|
75
|
+
elif char == quote_char:
|
|
76
|
+
in_string = False
|
|
77
|
+
quote_char = None
|
|
78
|
+
|
|
79
|
+
# 在字符串外检查注释标记
|
|
80
|
+
elif not in_string:
|
|
81
|
+
if char == "#":
|
|
82
|
+
return line[:i].rstrip()
|
|
83
|
+
elif char == "/" and i + 1 < len(line) and line[i + 1] == "/":
|
|
84
|
+
return line[:i].rstrip()
|
|
85
|
+
|
|
86
|
+
i += 1
|
|
87
|
+
|
|
88
|
+
return line
|
ddns/util/fileio.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
File I/O utilities for DDNS with Python 2/3 compatibility
|
|
4
|
+
@author: NewFuture
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from io import open # Python 2/3 compatible UTF-8 file operations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _ensure_directory_exists(file_path): # type: (str) -> None
|
|
12
|
+
"""
|
|
13
|
+
Internal helper to ensure directory exists for the given file path
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
file_path (str): File path whose directory should be created
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
OSError: If directory cannot be created
|
|
20
|
+
"""
|
|
21
|
+
directory = os.path.dirname(file_path)
|
|
22
|
+
if directory and not os.path.exists(directory):
|
|
23
|
+
os.makedirs(directory)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_file_safely(file_path, encoding="utf-8", default=None): # type: (str, str, str|None) -> str
|
|
27
|
+
"""
|
|
28
|
+
Safely read file content with UTF-8 encoding, return None if file doesn't exist or can't be read
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
file_path (str): Path to the file to read
|
|
32
|
+
encoding (str): File encoding (default: utf-8)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str | None: File content or None if failed
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
return read_file(file_path, encoding)
|
|
39
|
+
except Exception:
|
|
40
|
+
return default # type: ignore
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def write_file_safely(file_path, content, encoding="utf-8"): # type: (str, str, str) -> bool
|
|
44
|
+
"""
|
|
45
|
+
Safely write content to file with UTF-8 encoding
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
file_path (str): Path to the file to write
|
|
49
|
+
content (str): Content to write
|
|
50
|
+
encoding (str): File encoding (default: utf-8)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
bool: True if write successful, False otherwise
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
write_file(file_path, content, encoding)
|
|
57
|
+
return True
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_file(file_path, encoding="utf-8"): # type: (str, str) -> str
|
|
63
|
+
"""
|
|
64
|
+
Read file content with UTF-8 encoding, raise exception if failed
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
file_path (str): Path to the file to read
|
|
68
|
+
encoding (str): File encoding (default: utf-8)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
str: File content
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
IOError: If file cannot be read
|
|
75
|
+
UnicodeDecodeError: If file cannot be decoded with specified encoding
|
|
76
|
+
"""
|
|
77
|
+
with open(file_path, "r", encoding=encoding) as f:
|
|
78
|
+
return f.read()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def write_file(file_path, content, encoding="utf-8"): # type: (str, str, str) -> None
|
|
82
|
+
"""
|
|
83
|
+
Write content to file with UTF-8 encoding, raise exception if failed
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
file_path (str): Path to the file to write
|
|
87
|
+
content (str): Content to write
|
|
88
|
+
encoding (str): File encoding (default: utf-8)
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
IOError: If file cannot be written
|
|
92
|
+
UnicodeEncodeError: If content cannot be encoded with specified encoding
|
|
93
|
+
"""
|
|
94
|
+
_ensure_directory_exists(file_path)
|
|
95
|
+
with open(file_path, "w", encoding=encoding) as f:
|
|
96
|
+
f.write(content)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def ensure_directory(file_path): # type: (str) -> bool
|
|
100
|
+
"""
|
|
101
|
+
Ensure the directory for the given file path exists
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
file_path (str): File path whose directory should be created
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
bool: True if directory exists or was created successfully
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
_ensure_directory_exists(file_path)
|
|
111
|
+
return True
|
|
112
|
+
except (OSError, IOError):
|
|
113
|
+
return False
|