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/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
|
ddns/provider/tencentcloud.py
CHANGED
|
@@ -5,9 +5,11 @@ Tencent Cloud DNSPod API
|
|
|
5
5
|
|
|
6
6
|
@author: NewFuture
|
|
7
7
|
"""
|
|
8
|
-
|
|
9
|
-
from time import
|
|
10
|
-
|
|
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
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class TencentCloudProvider(BaseProvider):
|
|
@@ -19,7 +21,7 @@ class TencentCloudProvider(BaseProvider):
|
|
|
19
21
|
Documentation: https://cloud.tencent.com/document/api/1427
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
endpoint = "https://dnspod.tencentcloudapi.com"
|
|
23
25
|
content_type = TYPE_JSON
|
|
24
26
|
|
|
25
27
|
# 腾讯云 DNSPod API 配置
|
|
@@ -42,26 +44,23 @@ class TencentCloudProvider(BaseProvider):
|
|
|
42
44
|
"""
|
|
43
45
|
# 构建请求体
|
|
44
46
|
params = {k: v for k, v in params.items() if v is not None}
|
|
45
|
-
body =
|
|
47
|
+
body = self._encode_body(params)
|
|
46
48
|
|
|
47
49
|
# 构建请求头,小写 腾讯云只签名特定头部
|
|
48
|
-
headers = {
|
|
49
|
-
"content-type": self.content_type,
|
|
50
|
-
"host": self.API.split("://", 1)[1].strip("/"),
|
|
51
|
-
}
|
|
50
|
+
headers = {"content-type": self.content_type, "host": self.endpoint.split("://", 1)[1].strip("/")}
|
|
52
51
|
|
|
53
52
|
# 腾讯云特殊的密钥派生过程
|
|
54
53
|
date = strftime("%Y-%m-%d", gmtime())
|
|
55
54
|
credential_scope = "{}/{}/tc3_request".format(date, self.service)
|
|
56
55
|
|
|
57
56
|
# 派生签名密钥
|
|
58
|
-
secret_date = hmac_sha256("TC3" + self.
|
|
57
|
+
secret_date = hmac_sha256("TC3" + self.token, date).digest()
|
|
59
58
|
secret_service = hmac_sha256(secret_date, self.service).digest()
|
|
60
59
|
signing_key = hmac_sha256(secret_service, "tc3_request").digest()
|
|
61
60
|
|
|
62
61
|
# 预处理模板字符串
|
|
63
62
|
auth_format = "TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
|
|
64
|
-
self.
|
|
63
|
+
self.id,
|
|
65
64
|
credential_scope,
|
|
66
65
|
)
|
|
67
66
|
timestamp = str(int(time()))
|
|
@@ -132,7 +131,7 @@ class TencentCloudProvider(BaseProvider):
|
|
|
132
131
|
RecordType=record_type,
|
|
133
132
|
RecordLine=line,
|
|
134
133
|
**extra
|
|
135
|
-
)
|
|
134
|
+
) # fmt: skip
|
|
136
135
|
if not response or "RecordList" not in response:
|
|
137
136
|
self.logger.debug("No records found or query failed")
|
|
138
137
|
return None
|
|
@@ -165,7 +164,7 @@ class TencentCloudProvider(BaseProvider):
|
|
|
165
164
|
RecordLine=line or "默认",
|
|
166
165
|
TTL=int(ttl) if ttl else None,
|
|
167
166
|
**extra
|
|
168
|
-
)
|
|
167
|
+
) # fmt: skip
|
|
169
168
|
if response and "RecordId" in response:
|
|
170
169
|
self.logger.info("Record created successfully with ID: %s", response["RecordId"])
|
|
171
170
|
return True
|
|
@@ -186,7 +185,7 @@ class TencentCloudProvider(BaseProvider):
|
|
|
186
185
|
Value=value,
|
|
187
186
|
TTL=int(ttl) if ttl else None,
|
|
188
187
|
**extra
|
|
189
|
-
)
|
|
188
|
+
) # fmt: skip
|
|
190
189
|
if response and "RecordId" in response:
|
|
191
190
|
self.logger.info("Record updated successfully")
|
|
192
191
|
return True
|
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 | None
|
|
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
|
|
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
|