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/noip.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
No-IP (noip.com) Dynamic DNS API
|
|
4
|
+
@author: GitHub Copilot
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
from ._base import SimpleProvider, TYPE_FORM
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NoipProvider(SimpleProvider):
|
|
12
|
+
"""
|
|
13
|
+
No-IP (www.noip.com) Dynamic DNS Provider
|
|
14
|
+
|
|
15
|
+
No-IP is a popular dynamic DNS service that provides simple HTTP-based
|
|
16
|
+
API for updating DNS records. This provider supports the standard
|
|
17
|
+
No-IP update protocol.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
endpoint = "https://dynupdate.no-ip.com"
|
|
21
|
+
content_type = TYPE_FORM
|
|
22
|
+
accept = None # No-IP returns plain text response
|
|
23
|
+
decode_response = False # Response is plain text, not JSON
|
|
24
|
+
|
|
25
|
+
def _validate(self):
|
|
26
|
+
"""
|
|
27
|
+
Validate authentication credentials for No-IP
|
|
28
|
+
"""
|
|
29
|
+
if not self.id:
|
|
30
|
+
raise ValueError("No-IP requires username as 'id'")
|
|
31
|
+
if not self.token:
|
|
32
|
+
raise ValueError("No-IP requires password as 'token'")
|
|
33
|
+
|
|
34
|
+
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
|
|
35
|
+
"""
|
|
36
|
+
Update DNS record using No-IP Dynamic Update API
|
|
37
|
+
|
|
38
|
+
No-IP API Reference:
|
|
39
|
+
- URL: https://dynupdate.no-ip.com/nic/update
|
|
40
|
+
- Method: GET or POST
|
|
41
|
+
- Authentication: HTTP Basic Auth (username:password)
|
|
42
|
+
- Parameters:
|
|
43
|
+
- hostname: The hostname to update
|
|
44
|
+
- myip: The IP address to set (optional, uses client IP
|
|
45
|
+
if not provided)
|
|
46
|
+
|
|
47
|
+
Response codes:
|
|
48
|
+
- good: Update successful
|
|
49
|
+
- nochg: IP address is current, no update performed
|
|
50
|
+
- nohost: Hostname supplied does not exist
|
|
51
|
+
- badauth: Invalid username/password combination
|
|
52
|
+
- badagent: Client disabled
|
|
53
|
+
- !donator: An update request was sent including a feature that
|
|
54
|
+
is not available
|
|
55
|
+
- abuse: Username is blocked due to abuse
|
|
56
|
+
"""
|
|
57
|
+
self.logger.info("%s => %s(%s)", domain, value, record_type)
|
|
58
|
+
|
|
59
|
+
# Prepare request parameters
|
|
60
|
+
params = {"hostname": domain, "myip": value}
|
|
61
|
+
|
|
62
|
+
# Prepare HTTP Basic Authentication headers
|
|
63
|
+
auth_string = "{0}:{1}".format(self.id, self.token)
|
|
64
|
+
if not isinstance(auth_string, bytes): # Python 3
|
|
65
|
+
auth_bytes = auth_string.encode("utf-8")
|
|
66
|
+
else: # Python 2
|
|
67
|
+
auth_bytes = auth_string
|
|
68
|
+
|
|
69
|
+
auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
|
|
70
|
+
headers = {
|
|
71
|
+
"Authorization": "Basic {0}".format(auth_b64),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Use GET request as it's the most common method for DDNS
|
|
76
|
+
response = self._http("GET", "/nic/update", queries=params, headers=headers)
|
|
77
|
+
|
|
78
|
+
if response is not None:
|
|
79
|
+
response_str = str(response).strip()
|
|
80
|
+
self.logger.info("No-IP API response: %s", response_str)
|
|
81
|
+
|
|
82
|
+
# Check for successful responses
|
|
83
|
+
if response_str.startswith("good") or response_str.startswith("nochg"):
|
|
84
|
+
return True
|
|
85
|
+
elif response_str.startswith("nohost"):
|
|
86
|
+
self.logger.error("Hostname %s does not exist under No-IP account", domain)
|
|
87
|
+
elif response_str.startswith("badauth"):
|
|
88
|
+
self.logger.error("Invalid No-IP username/password combination")
|
|
89
|
+
elif response_str.startswith("badagent"):
|
|
90
|
+
self.logger.error("No-IP client disabled")
|
|
91
|
+
elif response_str.startswith("!donator"):
|
|
92
|
+
self.logger.error("Feature not available for No-IP free account")
|
|
93
|
+
elif response_str.startswith("abuse"):
|
|
94
|
+
self.logger.error("No-IP account blocked due to abuse")
|
|
95
|
+
else:
|
|
96
|
+
self.logger.error("Unexpected No-IP API response: %s", response_str)
|
|
97
|
+
else:
|
|
98
|
+
self.logger.error("Empty response from No-IP API")
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self.logger.error("Error updating No-IP record for %s: %s", domain, e)
|
|
102
|
+
|
|
103
|
+
return False
|
ddns/provider/tencentcloud.py
CHANGED
|
@@ -5,9 +5,9 @@ Tencent Cloud DNSPod API
|
|
|
5
5
|
|
|
6
6
|
@author: NewFuture
|
|
7
7
|
"""
|
|
8
|
-
from ._base import BaseProvider, TYPE_JSON
|
|
8
|
+
from ._base import BaseProvider, TYPE_JSON
|
|
9
|
+
from ._signature import hmac_sha256_authorization, sha256_hash, hmac_sha256
|
|
9
10
|
from time import time, strftime, gmtime
|
|
10
|
-
from json import dumps as jsonencode
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class TencentCloudProvider(BaseProvider):
|
|
@@ -19,7 +19,7 @@ class TencentCloudProvider(BaseProvider):
|
|
|
19
19
|
Documentation: https://cloud.tencent.com/document/api/1427
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
endpoint = "https://dnspod.tencentcloudapi.com"
|
|
23
23
|
content_type = TYPE_JSON
|
|
24
24
|
|
|
25
25
|
# 腾讯云 DNSPod API 配置
|
|
@@ -42,12 +42,12 @@ class TencentCloudProvider(BaseProvider):
|
|
|
42
42
|
"""
|
|
43
43
|
# 构建请求体
|
|
44
44
|
params = {k: v for k, v in params.items() if v is not None}
|
|
45
|
-
body =
|
|
45
|
+
body = self._encode_body(params)
|
|
46
46
|
|
|
47
47
|
# 构建请求头,小写 腾讯云只签名特定头部
|
|
48
48
|
headers = {
|
|
49
49
|
"content-type": self.content_type,
|
|
50
|
-
"host": self.
|
|
50
|
+
"host": self.endpoint.split("://", 1)[1].strip("/"),
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
# 腾讯云特殊的密钥派生过程
|
|
@@ -55,13 +55,13 @@ class TencentCloudProvider(BaseProvider):
|
|
|
55
55
|
credential_scope = "{}/{}/tc3_request".format(date, self.service)
|
|
56
56
|
|
|
57
57
|
# 派生签名密钥
|
|
58
|
-
secret_date = hmac_sha256("TC3" + self.
|
|
58
|
+
secret_date = hmac_sha256("TC3" + self.token, date).digest()
|
|
59
59
|
secret_service = hmac_sha256(secret_date, self.service).digest()
|
|
60
60
|
signing_key = hmac_sha256(secret_service, "tc3_request").digest()
|
|
61
61
|
|
|
62
62
|
# 预处理模板字符串
|
|
63
63
|
auth_format = "TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
|
|
64
|
-
self.
|
|
64
|
+
self.id,
|
|
65
65
|
credential_scope,
|
|
66
66
|
)
|
|
67
67
|
timestamp = str(int(time()))
|
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/http.py
CHANGED
|
@@ -13,13 +13,20 @@ import ssl
|
|
|
13
13
|
import os
|
|
14
14
|
|
|
15
15
|
try: # python 3
|
|
16
|
-
from
|
|
17
|
-
from urllib.parse import
|
|
16
|
+
from urllib.request import Request, HTTPSHandler, ProxyHandler, build_opener, OpenerDirector # noqa: F401
|
|
17
|
+
from urllib.parse import quote, urlencode
|
|
18
|
+
from urllib.error import HTTPError, URLError
|
|
18
19
|
except ImportError: # python 2
|
|
19
|
-
from
|
|
20
|
-
from
|
|
20
|
+
from urllib2 import Request, HTTPSHandler, ProxyHandler, build_opener, HTTPError, URLError # type: ignore[no-redef]
|
|
21
|
+
from urllib import urlencode, quote # type: ignore[no-redef]
|
|
21
22
|
|
|
22
|
-
__all__ = [
|
|
23
|
+
__all__ = [
|
|
24
|
+
"send_http_request",
|
|
25
|
+
"HttpResponse",
|
|
26
|
+
"quote",
|
|
27
|
+
"urlencode",
|
|
28
|
+
"URLError",
|
|
29
|
+
]
|
|
23
30
|
|
|
24
31
|
logger = getLogger().getChild(__name__)
|
|
25
32
|
|
|
@@ -28,14 +35,14 @@ class HttpResponse(object):
|
|
|
28
35
|
"""HTTP响应封装类"""
|
|
29
36
|
|
|
30
37
|
def __init__(self, status, reason, headers, body):
|
|
31
|
-
# type: (int, str,
|
|
38
|
+
# type: (int, str, object, str) -> None
|
|
32
39
|
"""
|
|
33
40
|
初始化HTTP响应对象
|
|
34
41
|
|
|
35
42
|
Args:
|
|
36
43
|
status (int): HTTP状态码
|
|
37
44
|
reason (str): 状态原因短语
|
|
38
|
-
headers (
|
|
45
|
+
headers (object): 响应头对象,直接使用 response.info()
|
|
39
46
|
body (str): 响应体内容
|
|
40
47
|
"""
|
|
41
48
|
self.status = status
|
|
@@ -46,51 +53,42 @@ class HttpResponse(object):
|
|
|
46
53
|
def get_header(self, name, default=None):
|
|
47
54
|
# type: (str, str | None) -> str | None
|
|
48
55
|
"""
|
|
49
|
-
|
|
56
|
+
获取指定名称的头部值
|
|
50
57
|
|
|
51
58
|
Args:
|
|
52
59
|
name (str): 头部名称
|
|
60
|
+
default (str | None): 默认值
|
|
53
61
|
|
|
54
62
|
Returns:
|
|
55
|
-
str | None:
|
|
63
|
+
str | None: 头部值,如果不存在则返回默认值
|
|
56
64
|
"""
|
|
57
|
-
|
|
58
|
-
for header_name, header_value in self.headers:
|
|
59
|
-
if header_name.lower() == name_lower:
|
|
60
|
-
return header_value
|
|
61
|
-
return default
|
|
65
|
+
return self.headers.get(name, default) # type: ignore[union-attr]
|
|
62
66
|
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
# type: (str, int | None, bool, str | None, bool | str) -> HTTPConnection | HTTPSConnection
|
|
66
|
-
"""创建HTTP/HTTPS连接"""
|
|
67
|
-
target = proxy or hostname
|
|
68
|
+
# 移除了自定义重定向处理器,使用urllib2/urllib.request的内置重定向处理
|
|
68
69
|
|
|
69
|
-
if not is_https:
|
|
70
|
-
conn = HTTPConnection(target, port)
|
|
71
|
-
else:
|
|
72
|
-
ssl_context = ssl.create_default_context()
|
|
73
|
-
|
|
74
|
-
if verify_ssl is False:
|
|
75
|
-
# 禁用SSL验证
|
|
76
|
-
ssl_context.check_hostname = False
|
|
77
|
-
ssl_context.verify_mode = ssl.CERT_NONE
|
|
78
|
-
elif hasattr(verify_ssl, "lower") and verify_ssl.lower() not in ("auto", "true"): # type: ignore[union-attr]
|
|
79
|
-
# 使用自定义CA证书 lower 判断 str/unicode 兼容 python2
|
|
80
|
-
try:
|
|
81
|
-
ssl_context.load_verify_locations(verify_ssl) # type: ignore[arg-type]
|
|
82
|
-
except Exception as e:
|
|
83
|
-
logger.error("Failed to load CA certificate from %s: %s", verify_ssl, e)
|
|
84
|
-
else:
|
|
85
|
-
# 默认验证,尝试加载系统证书
|
|
86
|
-
_load_system_ca_certs(ssl_context)
|
|
87
|
-
conn = HTTPSConnection(target, port, context=ssl_context)
|
|
88
70
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
71
|
+
def _create_ssl_context(verify_ssl):
|
|
72
|
+
# type: (bool | str) -> ssl.SSLContext | None
|
|
73
|
+
"""创建SSL上下文"""
|
|
74
|
+
ssl_context = ssl.create_default_context()
|
|
92
75
|
|
|
93
|
-
|
|
76
|
+
if verify_ssl is False:
|
|
77
|
+
# 禁用SSL验证
|
|
78
|
+
ssl_context.check_hostname = False
|
|
79
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
80
|
+
elif hasattr(verify_ssl, "lower") and verify_ssl.lower() not in ("auto", "true"): # type: ignore[union-attr]
|
|
81
|
+
# 使用自定义CA证书
|
|
82
|
+
try:
|
|
83
|
+
ssl_context.load_verify_locations(verify_ssl) # type: ignore[arg-type]
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error("Failed to load CA certificate from %s: %s", verify_ssl, e)
|
|
86
|
+
return None
|
|
87
|
+
else:
|
|
88
|
+
# 默认验证,尝试加载系统证书
|
|
89
|
+
_load_system_ca_certs(ssl_context)
|
|
90
|
+
|
|
91
|
+
return ssl_context
|
|
94
92
|
|
|
95
93
|
|
|
96
94
|
def _load_system_ca_certs(ssl_context):
|
|
@@ -123,125 +121,78 @@ def _load_system_ca_certs(ssl_context):
|
|
|
123
121
|
loaded_count += 1
|
|
124
122
|
logger.debug("Loaded CA certificates from: %s", ca_path)
|
|
125
123
|
except Exception as e:
|
|
126
|
-
logger.
|
|
124
|
+
logger.info("Failed to load CA certificates from %s: %s", ca_path, e)
|
|
127
125
|
|
|
128
|
-
if loaded_count > 0:
|
|
129
|
-
logger.debug("Successfully loaded CA certificates from %d locations", loaded_count)
|
|
130
126
|
|
|
127
|
+
def _create_opener(proxy, verify_ssl):
|
|
128
|
+
# type: (str | None, bool | str) -> OpenerDirector
|
|
129
|
+
"""创建URL打开器,支持代理和SSL配置"""
|
|
130
|
+
handlers = []
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
132
|
+
if proxy:
|
|
133
|
+
handlers.append(ProxyHandler({"http": proxy, "https": proxy}))
|
|
134
|
+
|
|
135
|
+
ssl_context = _create_ssl_context(verify_ssl)
|
|
136
|
+
if ssl_context:
|
|
137
|
+
handlers.append(HTTPSHandler(context=ssl_context))
|
|
138
|
+
|
|
139
|
+
return build_opener(*handlers)
|
|
139
140
|
|
|
140
141
|
|
|
141
|
-
def send_http_request(method, url, body=None, headers=None, proxy=None,
|
|
142
|
-
# type: (str, str, str | bytes | None, dict[str, str] | None, str | None,
|
|
142
|
+
def send_http_request(method, url, body=None, headers=None, proxy=None, verify_ssl=True):
|
|
143
|
+
# type: (str, str, str | bytes | None, dict[str, str] | None, str | None, bool | str) -> HttpResponse
|
|
143
144
|
"""
|
|
144
145
|
发送HTTP/HTTPS请求,支持重定向跟随和灵活的SSL验证
|
|
145
|
-
|
|
146
|
+
|
|
146
147
|
Args:
|
|
147
148
|
method (str): HTTP方法,如GET、POST等
|
|
148
149
|
url (str): 请求的URL
|
|
149
150
|
body (str | bytes | None): 请求体
|
|
150
151
|
headers (dict[str, str] | None): 请求头
|
|
151
152
|
proxy (str | None): 代理地址
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
verify_ssl (bool | str): SSL验证配置
|
|
154
|
+
|
|
154
155
|
Returns:
|
|
155
|
-
HttpResponse:
|
|
156
|
+
HttpResponse: 响应对象
|
|
157
|
+
|
|
156
158
|
Raises:
|
|
157
|
-
|
|
159
|
+
URLError: 如果请求失败
|
|
158
160
|
ssl.SSLError: 如果SSL验证失败
|
|
159
161
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
# 准备请求
|
|
163
|
+
if isinstance(body, str):
|
|
164
|
+
body = body.encode("utf-8")
|
|
165
|
+
|
|
166
|
+
req = Request(url, data=body)
|
|
167
|
+
req.get_method = lambda: method # type: ignore[attr-defined]
|
|
162
168
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
hostname = url_obj.hostname or url_obj.netloc.split(":")[0]
|
|
167
|
-
request_path = "{}?{}".format(url_obj.path, url_obj.query) if url_obj.query else url_obj.path
|
|
168
|
-
headers = headers or {}
|
|
169
|
+
if headers:
|
|
170
|
+
for key, value in headers.items():
|
|
171
|
+
req.add_header(key, value)
|
|
169
172
|
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
conn = _create_connection(hostname, url_obj.port, is_https, proxy, verify_ssl)
|
|
173
|
+
# 创建opener并发送请求
|
|
174
|
+
opener = _create_opener(proxy, verify_ssl)
|
|
173
175
|
|
|
174
|
-
# 执行请求,处理SSL错误
|
|
175
176
|
try:
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
response = opener.open(req)
|
|
178
|
+
response_headers = response.info()
|
|
179
|
+
raw_body = response.read()
|
|
180
|
+
decoded_body = _decode_response_body(raw_body, response_headers.get("Content-Type"))
|
|
181
|
+
return HttpResponse(response.getcode(), getattr(response, "msg", ""), response_headers, decoded_body)
|
|
182
|
+
except HTTPError as e:
|
|
183
|
+
# 记录HTTP错误并读取响应体用于调试
|
|
184
|
+
response_headers = getattr(e, "headers", {})
|
|
185
|
+
raw_body = e.read()
|
|
186
|
+
decoded_body = _decode_response_body(raw_body, response_headers.get("Content-Type"))
|
|
187
|
+
logger.error("HTTP error %s: %s for %s", e.code, getattr(e, "reason", str(e)), url)
|
|
188
|
+
return HttpResponse(e.code, getattr(e, "reason", str(e)), response_headers, decoded_body)
|
|
178
189
|
except ssl.SSLError:
|
|
179
|
-
|
|
180
|
-
if verify_ssl == "auto" and is_https:
|
|
190
|
+
if verify_ssl == "auto":
|
|
181
191
|
logger.warning("SSL verification failed, switching to unverified connection %s", url)
|
|
182
|
-
|
|
183
|
-
conn = _create_connection(hostname, url_obj.port, is_https, proxy, False)
|
|
184
|
-
conn.request(method, request_path, body, headers)
|
|
185
|
-
response = conn.getresponse()
|
|
186
|
-
actual_verify_ssl = False
|
|
192
|
+
return send_http_request(method, url, body, headers, proxy, False)
|
|
187
193
|
else:
|
|
188
194
|
raise
|
|
189
195
|
|
|
190
|
-
# 检查重定向
|
|
191
|
-
status = response.status
|
|
192
|
-
if 300 <= status < 400:
|
|
193
|
-
location = response.getheader("Location")
|
|
194
|
-
_close_connection(conn)
|
|
195
|
-
if not location:
|
|
196
|
-
# 无Location头的重定向
|
|
197
|
-
logger.warning("Redirect status %d but no Location header", status)
|
|
198
|
-
location = ""
|
|
199
|
-
|
|
200
|
-
# 构建重定向URL
|
|
201
|
-
redirect_url = _build_redirect_url(location, "{}://{}".format(url_obj.scheme, url_obj.netloc), url_obj.path)
|
|
202
|
-
|
|
203
|
-
# 如果重定向URL没有查询字符串,但原始URL有,则附加
|
|
204
|
-
if url_obj.query and "?" not in redirect_url:
|
|
205
|
-
redirect_url += "?" + url_obj.query
|
|
206
|
-
|
|
207
|
-
# 确定重定向方法:303或302+POST转为GET,其他保持原方法
|
|
208
|
-
if status == 303 or (status == 302 and method == "POST"):
|
|
209
|
-
method, body = "GET", None
|
|
210
|
-
# 如果从POST转为GET,移除相关的头部
|
|
211
|
-
if headers:
|
|
212
|
-
headers = {k: v for k, v in headers.items() if k.lower() not in ("content-length", "content-type")}
|
|
213
|
-
|
|
214
|
-
logger.info("Redirecting [%d] to: %s", status, redirect_url)
|
|
215
|
-
# 递归处理重定向
|
|
216
|
-
return send_http_request(method, redirect_url, body, headers, proxy, max_redirects - 1, actual_verify_ssl)
|
|
217
|
-
|
|
218
|
-
# 处理最终响应
|
|
219
|
-
content_type = response.getheader("Content-Type")
|
|
220
|
-
response_headers = response.getheaders()
|
|
221
|
-
raw_body = response.read()
|
|
222
|
-
_close_connection(conn)
|
|
223
|
-
|
|
224
|
-
# 解码响应体并创建响应对象
|
|
225
|
-
decoded_body = _decode_response_body(raw_body, content_type)
|
|
226
|
-
return HttpResponse(status, response.reason, response_headers, decoded_body)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def _build_redirect_url(location, base, path):
|
|
230
|
-
# type: (str, str, str) -> str
|
|
231
|
-
"""构建重定向URL,使用简单的字符串操作"""
|
|
232
|
-
if location.startswith("http"):
|
|
233
|
-
return location
|
|
234
|
-
|
|
235
|
-
if location.startswith("/"):
|
|
236
|
-
# 绝对路径:使用base的scheme和netloc
|
|
237
|
-
base_url = urlparse(base)
|
|
238
|
-
return "{}://{}{}".format(base_url.scheme, base_url.netloc, location)
|
|
239
|
-
else:
|
|
240
|
-
base_path = path.rsplit("/", 1)[0] if "/" in path else ""
|
|
241
|
-
if not base_path.endswith("/"):
|
|
242
|
-
base_path += "/"
|
|
243
|
-
return base + base_path + location
|
|
244
|
-
|
|
245
196
|
|
|
246
197
|
def _decode_response_body(raw_body, content_type):
|
|
247
198
|
# type: (bytes, str | None) -> str
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddns
|
|
3
|
-
Version: 4.1.
|
|
3
|
+
Version: 4.1.0b2
|
|
4
4
|
Summary: Dynamic DNS client for multiple providers, supporting IPv4 and IPv6.
|
|
5
5
|
Author-email: NewFuture <python@newfuture.cc>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -84,12 +84,16 @@ Dynamic: license-file
|
|
|
84
84
|
- 服务商支持:
|
|
85
85
|
- [DNSPOD](https://www.dnspod.cn/) ([配置指南](doc/providers/dnspod.md))
|
|
86
86
|
- [阿里 DNS](http://www.alidns.com/) ([配置指南](doc/providers/alidns.md)) ⚡
|
|
87
|
-
- [
|
|
88
|
-
- [
|
|
89
|
-
- [
|
|
90
|
-
- [
|
|
91
|
-
- [
|
|
87
|
+
- [阿里云边缘安全加速(ESA)](https://esa.console.aliyun.com/) ([配置指南](doc/providers/aliesa.md)) ⚡
|
|
88
|
+
- [DNS.COM](https://www.dns.com/) ([配置指南](doc/providers/51dns.md)) (@loftor-git)
|
|
89
|
+
- [DNSPOD 国际版](https://www.dnspod.com/) ([配置指南](doc/providers/dnspod_com.md))
|
|
90
|
+
- [CloudFlare](https://www.cloudflare.com/) ([配置指南](doc/providers/cloudflare.md)) (@tongyifan)
|
|
91
|
+
- [HE.net](https://dns.he.net/) ([配置指南](doc/providers/he.md)) (@NN708) (不支持自动创建记录)
|
|
92
|
+
- [华为云](https://huaweicloud.com/) ([配置指南](doc/providers/huaweidns.md)) (@cybmp3) ⚡
|
|
93
|
+
- [NameSilo](https://www.namesilo.com/) ([配置指南](doc/providers/namesilo.md))
|
|
92
94
|
- [腾讯云](https://cloud.tencent.com/) ([配置指南](doc/providers/tencentcloud.md)) ⚡
|
|
95
|
+
- [腾讯云 EdgeOne](https://cloud.tencent.com/product/teo) ([配置指南](doc/providers/edgeone.md)) ⚡
|
|
96
|
+
- [No-IP](https://www.noip.com/) ([配置指南](doc/providers/noip.md))
|
|
93
97
|
- 自定义回调 API ([配置指南](doc/providers/callback.md))
|
|
94
98
|
|
|
95
99
|
> ⚡ 标记的服务商使用高级 HMAC-SHA256 签名认证,提供企业级安全保障
|
|
@@ -161,12 +165,16 @@ Dynamic: license-file
|
|
|
161
165
|
|
|
162
166
|
- **DNSPOD(中国版)**: [创建 token](https://support.dnspod.cn/Kb/showarticle/tsid/227/) | [详细配置文档](doc/providers/dnspod.md)
|
|
163
167
|
- **阿里云 DNS**: [申请 accesskey](https://help.aliyun.com/document_detail/87745.htm) | [详细配置文档](doc/providers/alidns.md)
|
|
164
|
-
-
|
|
165
|
-
- **
|
|
166
|
-
- **
|
|
167
|
-
- **
|
|
168
|
-
-
|
|
168
|
+
- **阿里云边缘安全加速(ESA)**: [申请 accesskey](https://help.aliyun.com/document_detail/87745.htm) | [详细配置文档](doc/providers/aliesa.md)
|
|
169
|
+
- **51DNS(dns.com)**: [API Key/Secret](https://www.dns.com/member/apiSet) | [详细配置文档](doc/providers/51dns.md)
|
|
170
|
+
- **DNSPOD(国际版)**: [获取 token](https://www.dnspod.com/docs/info.html#get-the-user-token) | [详细配置文档](doc/providers/dnspod_com.md)
|
|
171
|
+
- **CloudFlare**: [API Key](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-)(除了 `email + API KEY`,也可使用 `Token`,**需要list Zone 权限**) | [详细配置文档](doc/providers/cloudflare.md)
|
|
172
|
+
- **HE.net**: [DDNS 文档](https://dns.he.net/docs.html)(仅需将设置的密码填入 `token` 字段,`id` 字段可留空) | [详细配置文档](doc/providers/he.md)
|
|
173
|
+
- **华为云 DNS**: [APIKEY 申请](https://console.huaweicloud.com/iam/)(点左边访问密钥,然后点新增访问密钥) | [详细配置文档](doc/providers/huaweidns.md)
|
|
174
|
+
- **NameSilo**: [API Key](https://www.namesilo.com/account/api-manager)(API Manager 中获取 API Key) | [详细配置文档](doc/providers/namesilo.md)
|
|
169
175
|
- **腾讯云 DNS**: [详细配置文档](doc/providers/tencentcloud.md)
|
|
176
|
+
- **腾讯云 EdgeOne**: [详细配置文档](doc/providers/edgeone.md)
|
|
177
|
+
- **No-IP**: [用户名和密码](https://www.noip.com/)(使用 No-IP 账户的用户名和密码) | [详细配置文档](doc/providers/noip.md)
|
|
170
178
|
- **自定义回调**: 参数填写方式请查看下方的自定义回调配置说明
|
|
171
179
|
|
|
172
180
|
2. 修改配置文件,`ipv4` 和 `ipv6` 字段,为待更新的域名,详细参照配置说明
|
|
@@ -217,7 +225,7 @@ python -m ddns -c /path/to/config.json
|
|
|
217
225
|
| :----: | :----------------: | :------: | :---------: | :----------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
218
226
|
| id | string | √ | 无 | api 访问 ID | Cloudflare 为邮箱(使用 Token 时留空)<br>HE.net 可留空<br>华为云为 Access Key ID (AK) |
|
|
219
227
|
| token | string | √ | 无 | api 授权 token | 部分平台叫 secret key,**反馈粘贴时删除** |
|
|
220
|
-
| dns | string | No | `"dnspod"` | dns 服务商 | 阿里 DNS 为 `alidns`,Cloudflare 为 `cloudflare`,dns.com 为 `dnscom`,DNSPOD 国内为 `dnspod`,DNSPOD 国际为 `dnspod_com`,HE.net 为 `he`,华为云为 `huaweidns`,腾讯云为 `tencentcloud`,自定义回调为 `callback`。部分服务商有[详细配置文档](doc/providers/) |
|
|
228
|
+
| dns | string | No | `"dnspod"` | dns 服务商 | 阿里 DNS 为 `alidns`,阿里ESA为 `aliesa`,Cloudflare 为 `cloudflare`,dns.com 为 `dnscom`,DNSPOD 国内为 `dnspod`,DNSPOD 国际为 `dnspod_com`,HE.net 为 `he`,华为云为 `huaweidns`,NameSilo 为 `namesilo`,腾讯云为 `tencentcloud`,腾讯云EdgeOne为 `edgeone`,No-IP 为 `noip`,自定义回调为 `callback`。部分服务商有[详细配置文档](doc/providers/) |
|
|
221
229
|
| ipv4 | array | No | `[]` | ipv4 域名列表 | 为 `[]` 时,不会获取和更新 IPv4 地址 |
|
|
222
230
|
| ipv6 | array | No | `[]` | ipv6 域名列表 | 为 `[]` 时,不会获取和更新 IPv6 地址 |
|
|
223
231
|
| index4 | string\|int\|array | No | `"default"` | ipv4 获取方式 | 可设置 `网卡`、`内网`、`公网`、`正则` 等方式 |
|
|
@@ -266,7 +274,7 @@ python -m ddns -c /path/to/config.json
|
|
|
266
274
|
"$schema": "https://ddns.newfuture.cc/schema/v4.0.json",
|
|
267
275
|
"id": "12345",
|
|
268
276
|
"token": "mytokenkey",
|
|
269
|
-
"dns": "dnspod 或 dnspod_com 或 alidns 或 dnscom 或 cloudflare 或 he 或 huaweidns 或 tencentcloud 或 callback",
|
|
277
|
+
"dns": "dnspod 或 dnspod_com 或 alidns 或 aliesa 或 dnscom 或 cloudflare 或 he 或 huaweidns 或 namesilo 或 tencentcloud 或 noip 或 callback",
|
|
270
278
|
"ipv4": ["ddns.newfuture.cc", "ipv4.ddns.newfuture.cc"],
|
|
271
279
|
"ipv6": ["ddns.newfuture.cc", "ipv6.ddns.newfuture.cc"],
|
|
272
280
|
"index4": 0,
|
|
@@ -330,6 +338,7 @@ Docker 镜像在无额外参数的情况下,已默认启用每 5 分钟执行
|
|
|
330
338
|
使用系统自带的 IE 浏览器访问一次对应的 API 即可
|
|
331
339
|
|
|
332
340
|
- alidns 打开: <https://alidns.aliyuncs.com>
|
|
341
|
+
- aliesa 打开: <https://esa.cn-hangzhou.aliyuncs.com>
|
|
333
342
|
- cloudflare 打开: <https://api.cloudflare.com>
|
|
334
343
|
- dns.com 打开: <https://www.dns.com>
|
|
335
344
|
- dnspod.cn 打开: <https://dnsapi.cn>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
ddns/__builtins__.pyi,sha256=QTTtckbWAAZasu-uzeeyK4MLj9S9dxLtRlhty6U01jQ,109
|
|
2
|
+
ddns/__init__.py,sha256=SUqnkj9QXJFuc7vXN63wohdxYu2OupGgfTyZYYtO-uI,258
|
|
3
|
+
ddns/__main__.py,sha256=PrkwVahIUGvT9CNy3uXs8ZX-ox2RVx72aeWexhl6YZM,4134
|
|
4
|
+
ddns/cache.py,sha256=A1s3rnOJbPrGEjbGbbISrVg46lZFzZ84WPdzHaCtYBk,5971
|
|
5
|
+
ddns/ip.py,sha256=K4iFWwBX9kSrULVdNGX17dc_Zbqy9VpmpP2mdU_h6Cc,3947
|
|
6
|
+
ddns/provider/__init__.py,sha256=qAw-R-l7nUA9L96Tmr2n-T7_g8T9Mah7u615yg2zScY,2793
|
|
7
|
+
ddns/provider/_base.py,sha256=X73ID1iyz11zCX2nQne-8i7xW5CPGIXuWSs2zFI_Al8,20810
|
|
8
|
+
ddns/provider/_signature.py,sha256=fF8XxMDkXjia96d1gIVYWc72MVVe65AAnD7qr51qKXA,4140
|
|
9
|
+
ddns/provider/alidns.py,sha256=sBwDrtARbqgfEynPyR-dVfAVeh4K6lhO1XL9_0C1Tig,6096
|
|
10
|
+
ddns/provider/aliesa.py,sha256=5tGPyW22TtI7AGJKugIOipby3R68bHHEJiR_k9og1Rs,4695
|
|
11
|
+
ddns/provider/callback.py,sha256=WSd8QNpWUUJXQbyIatANmyigLU6VBvrLBsn7lALz8yA,2924
|
|
12
|
+
ddns/provider/cloudflare.py,sha256=0sVelYFU3ndjREQjtQAaT6G3jxj1Stk3hN7bT_NAUpI,4484
|
|
13
|
+
ddns/provider/debug.py,sha256=3TZYcXHcIzTwUEj5_rvZTT0QqS6jFwTuBS7cMujG2tM,587
|
|
14
|
+
ddns/provider/dnscom.py,sha256=IfdX_HaEjH12NHxdv6taRiX-zLQTo4kpBrJ1VWE9au4,3667
|
|
15
|
+
ddns/provider/dnspod.py,sha256=CSAIJYIjna-PphR4KE9KlMsPcgfEyRnJPCIEDqYlzNg,4445
|
|
16
|
+
ddns/provider/dnspod_com.py,sha256=uTFBSJIlFAijbpXYvte_pQHpGvTR4co9mEkJ0prRq7s,398
|
|
17
|
+
ddns/provider/edgeone.py,sha256=ck8GkkOk-Wa01QoGoPfD2pr6ok7gLuQyB7TDsSyNtHE,3897
|
|
18
|
+
ddns/provider/he.py,sha256=v6x_2_iJBr1RMefaBgnn3TWNk8-JxcL3llxmxr1vz1w,1820
|
|
19
|
+
ddns/provider/huaweidns.py,sha256=LH6EiCzGuftg3dNk7ZiPLaS6BvM0vWydrRXhtaLxj04,5590
|
|
20
|
+
ddns/provider/namesilo.py,sha256=jgqkQUcHVCyVjYoxazmcN9Gu_Zd-EAv3aSnNmHHJ1jk,5843
|
|
21
|
+
ddns/provider/noip.py,sha256=JY2939xCu0hF6wl1QPZpxz-2TgwYVY0GWMY97iRVHME,3961
|
|
22
|
+
ddns/provider/tencentcloud.py,sha256=SzQlTVU9W3NRq4QvK88eP4A3y6g13M0Ud-AE93qvSQ8,7153
|
|
23
|
+
ddns/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
ddns/util/comment.py,sha256=_B8sRpCJksNspsdld4ha03W1TAi-MyPRK7pL7ym0j9k,2199
|
|
25
|
+
ddns/util/http.py,sha256=xG5QyRO8r-pCnF4YBfGs_UxqUamX_f3n_wshw6FlMqM,7664
|
|
26
|
+
ddns-4.1.0b2.dist-info/licenses/LICENSE,sha256=MI-ECjp-Vl7WZLiSPY6r5VwrOReNiICVB1QCXiUGt_s,1111
|
|
27
|
+
ddns-4.1.0b2.dist-info/METADATA,sha256=bwPvJGxEL0JEahaLk3PD4gI04tZdVUpAJQQMKH0xP68,20807
|
|
28
|
+
ddns-4.1.0b2.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
29
|
+
ddns-4.1.0b2.dist-info/entry_points.txt,sha256=2-VbA-WZcjebkZrGKvUCuBBRYF4xQNMoLIoGaS234WU,44
|
|
30
|
+
ddns-4.1.0b2.dist-info/top_level.txt,sha256=Se0wn3T8Bc4pj55dGwVrCe8BFwmFCBwQVHF1bTyV0o0,5
|
|
31
|
+
ddns-4.1.0b2.dist-info/RECORD,,
|