ddns 4.1.0b2__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 -2
- ddns/__main__.py +35 -7
- ddns/ip.py +3 -5
- ddns/provider/_base.py +13 -26
- ddns/provider/alidns.py +5 -4
- ddns/provider/aliesa.py +5 -4
- ddns/provider/callback.py +1 -0
- ddns/provider/cloudflare.py +2 -2
- ddns/provider/debug.py +0 -1
- ddns/provider/dnscom.py +3 -2
- ddns/provider/dnspod.py +1 -3
- ddns/provider/edgeone.py +1 -0
- ddns/provider/huaweidns.py +5 -4
- ddns/provider/noip.py +14 -16
- ddns/provider/tencentcloud.py +9 -10
- ddns/util/fileio.py +113 -0
- ddns/util/http.py +271 -177
- {ddns-4.1.0b2.dist-info → ddns-4.1.0b3.dist-info}/METADATA +85 -118
- ddns-4.1.0b3.dist-info/RECORD +32 -0
- ddns-4.1.0b2.dist-info/RECORD +0 -31
- {ddns-4.1.0b2.dist-info → ddns-4.1.0b3.dist-info}/WHEEL +0 -0
- {ddns-4.1.0b2.dist-info → ddns-4.1.0b3.dist-info}/entry_points.txt +0 -0
- {ddns-4.1.0b2.dist-info → ddns-4.1.0b3.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.1.0b2.dist-info → ddns-4.1.0b3.dist-info}/top_level.txt +0 -0
ddns/__builtins__.pyi
CHANGED
ddns/__init__.py
CHANGED
ddns/__main__.py
CHANGED
|
@@ -10,14 +10,12 @@ from logging import getLogger
|
|
|
10
10
|
import sys
|
|
11
11
|
|
|
12
12
|
from .__init__ import __version__, __description__, build_date
|
|
13
|
-
from .config import
|
|
14
|
-
from .provider import get_provider_class, SimpleProvider
|
|
13
|
+
from .config import load_configs, Config # noqa: F401
|
|
14
|
+
from .provider import get_provider_class, SimpleProvider # noqa: F401
|
|
15
15
|
from . import ip
|
|
16
16
|
from .cache import Cache
|
|
17
17
|
|
|
18
18
|
logger = getLogger()
|
|
19
|
-
# Set user agent for All Providers
|
|
20
|
-
SimpleProvider.user_agent = SimpleProvider.user_agent.format(version=__version__)
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
def get_ip(ip_type, rules):
|
|
@@ -94,7 +92,7 @@ def run(config):
|
|
|
94
92
|
# dns provider class
|
|
95
93
|
provider_class = get_provider_class(config.dns)
|
|
96
94
|
dns = provider_class(
|
|
97
|
-
config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy,
|
|
95
|
+
config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy, ssl=config.ssl
|
|
98
96
|
)
|
|
99
97
|
cache = Cache.new(config.cache, config.md5(), logger)
|
|
100
98
|
return (
|
|
@@ -110,8 +108,38 @@ def main():
|
|
|
110
108
|
sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
111
109
|
sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
112
110
|
logger.name = "ddns"
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
|
|
112
|
+
# 使用多配置加载器,它会自动处理单个和多个配置
|
|
113
|
+
configs = load_configs(__description__, __version__, build_date)
|
|
114
|
+
|
|
115
|
+
if len(configs) == 1:
|
|
116
|
+
# 单个配置,使用原有逻辑(向后兼容)
|
|
117
|
+
config = configs[0]
|
|
118
|
+
success = run(config)
|
|
119
|
+
if not success:
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
else:
|
|
122
|
+
# 多个配置,使用新的批处理逻辑
|
|
123
|
+
overall_success = True
|
|
124
|
+
for i, config in enumerate(configs):
|
|
125
|
+
# 如果log_level有值则设置setLevel
|
|
126
|
+
if hasattr(config, "log_level") and config.log_level:
|
|
127
|
+
logger.setLevel(config.log_level)
|
|
128
|
+
logger.info("Running configuration %d/%d", i + 1, len(configs))
|
|
129
|
+
# 记录当前provider
|
|
130
|
+
logger.info("Using DNS provider: %s", config.dns)
|
|
131
|
+
success = run(config)
|
|
132
|
+
if not success:
|
|
133
|
+
overall_success = False
|
|
134
|
+
logger.error("Configuration %d failed", i + 1)
|
|
135
|
+
else:
|
|
136
|
+
logger.info("Configuration %d completed successfully", i + 1)
|
|
137
|
+
|
|
138
|
+
if not overall_success:
|
|
139
|
+
logger.error("Some configurations failed")
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
else:
|
|
142
|
+
logger.info("All configurations completed successfully")
|
|
115
143
|
|
|
116
144
|
|
|
117
145
|
if __name__ == "__main__":
|
ddns/ip.py
CHANGED
|
@@ -5,7 +5,7 @@ from os import name as os_name, popen
|
|
|
5
5
|
from socket import socket, getaddrinfo, gethostname, AF_INET, AF_INET6, SOCK_DGRAM
|
|
6
6
|
from logging import debug, error
|
|
7
7
|
|
|
8
|
-
from .util.http import
|
|
8
|
+
from .util.http import request
|
|
9
9
|
|
|
10
10
|
# 模块级别的SSL验证配置,默认使用auto模式
|
|
11
11
|
ssl_verify = "auto"
|
|
@@ -48,9 +48,8 @@ def local_v4(i=0): # 本地ipv4地址
|
|
|
48
48
|
def _open(url, reg):
|
|
49
49
|
try:
|
|
50
50
|
debug("open: %s", url)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
51
|
+
# IP 模块重试3次
|
|
52
|
+
response = request("GET", url, verify=ssl_verify, retries=2)
|
|
54
53
|
res = response.body
|
|
55
54
|
debug("response: %s", res)
|
|
56
55
|
match = compile(reg).search(res)
|
|
@@ -70,7 +69,6 @@ def public_v6(url="https://api-ipv6.ip.sb/ip", reg=IPV6_REG): # 公网IPV6地
|
|
|
70
69
|
|
|
71
70
|
|
|
72
71
|
def _ip_regex_match(parrent_regex, match_regex):
|
|
73
|
-
|
|
74
72
|
ip_pattern = compile(parrent_regex)
|
|
75
73
|
matcher = compile(match_regex)
|
|
76
74
|
|
ddns/provider/_base.py
CHANGED
|
@@ -58,7 +58,7 @@ Defines a unified interface to support extension and adaptation across providers
|
|
|
58
58
|
from abc import ABCMeta, abstractmethod
|
|
59
59
|
from json import loads as jsondecode, dumps as jsonencode
|
|
60
60
|
from logging import Logger, getLogger # noqa:F401 # type: ignore[no-redef]
|
|
61
|
-
from ..util.http import
|
|
61
|
+
from ..util.http import request, quote, urlencode
|
|
62
62
|
|
|
63
63
|
TYPE_FORM = "application/x-www-form-urlencoded"
|
|
64
64
|
TYPE_JSON = "application/json"
|
|
@@ -102,13 +102,11 @@ class SimpleProvider(object):
|
|
|
102
102
|
accept = TYPE_JSON # type: str | None
|
|
103
103
|
# Decode Response as JSON by default
|
|
104
104
|
decode_response = True # type: bool
|
|
105
|
-
# UA 可自定义, 可在子类中覆盖,空则不设置
|
|
106
|
-
user_agent = "DDNS/{version} (ddns@newfuture.cc)"
|
|
107
105
|
# Description
|
|
108
106
|
remark = "Managed by [DDNS](https://ddns.newfuture.cc)"
|
|
109
107
|
|
|
110
|
-
def __init__(self, id, token, logger=None,
|
|
111
|
-
# type: (str, str, Logger | None, bool|str,
|
|
108
|
+
def __init__(self, id, token, logger=None, ssl="auto", proxy=None, endpoint=None, **options):
|
|
109
|
+
# type: (str, str, Logger | None, bool|str, list[str]|None, str|None, **object) -> None
|
|
112
110
|
"""
|
|
113
111
|
初始化服务商对象
|
|
114
112
|
|
|
@@ -117,14 +115,18 @@ class SimpleProvider(object):
|
|
|
117
115
|
Args:
|
|
118
116
|
id (str): 身份认证 ID / Authentication ID
|
|
119
117
|
token (str): 密钥 / Authentication Token
|
|
118
|
+
proxy (list[str | None] | None): 代理配置,支持代理列表
|
|
120
119
|
options (dict): 其它参数 / Additional options
|
|
121
120
|
"""
|
|
122
121
|
self.id = id
|
|
123
122
|
self.token = token
|
|
124
123
|
if endpoint:
|
|
125
124
|
self.endpoint = endpoint
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
|
|
126
|
+
# 处理代理配置
|
|
127
|
+
self._proxy = proxy # 代理列表或None
|
|
128
|
+
|
|
129
|
+
self._ssl = ssl
|
|
128
130
|
|
|
129
131
|
self.options = options
|
|
130
132
|
name = self.__class__.__name__
|
|
@@ -227,26 +229,11 @@ class SimpleProvider(object):
|
|
|
227
229
|
# 处理headers
|
|
228
230
|
if self.accept and "accept" not in headers and "Accept" not in headers:
|
|
229
231
|
headers["accept"] = self.accept
|
|
230
|
-
if
|
|
231
|
-
headers["user-agent"] = self.user_agent
|
|
232
|
-
if len(headers) > 3:
|
|
232
|
+
if len(headers) > 2:
|
|
233
233
|
self.logger.debug("headers:\n%s", {k: self._mask_sensitive_data(v) for k, v in headers.items()})
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if p:
|
|
238
|
-
self.logger.debug("Using proxy: %s", p)
|
|
239
|
-
try:
|
|
240
|
-
response = send_http_request(
|
|
241
|
-
method, url, body=body_data, headers=headers, proxy=p, verify_ssl=self._ssl
|
|
242
|
-
)
|
|
243
|
-
break # 成功发送请求,跳出循环
|
|
244
|
-
except Exception as e:
|
|
245
|
-
self.logger.warning("Failed to send request: %s", e)
|
|
246
|
-
if not response:
|
|
247
|
-
if len(self._proxy) > 1:
|
|
248
|
-
self.logger.error("Failed to send request via all proxies: %s", self._proxy)
|
|
249
|
-
raise RuntimeError("Failed to send request to {}".format(url))
|
|
235
|
+
# 直接传递代理列表给request函数
|
|
236
|
+
response = request(method, url, body_data, headers=headers, proxies=self._proxy, verify=self._ssl, retries=2)
|
|
250
237
|
# 处理响应
|
|
251
238
|
status_code = response.status
|
|
252
239
|
if not (200 <= status_code < 300):
|
|
@@ -257,7 +244,7 @@ class SimpleProvider(object):
|
|
|
257
244
|
if status_code >= 500 or status_code in (400, 401, 403):
|
|
258
245
|
self.logger.error("HTTP error:\n%s", res)
|
|
259
246
|
if status_code == 400:
|
|
260
|
-
raise RuntimeError("
|
|
247
|
+
raise RuntimeError("参数错误 [400]: " + response.reason)
|
|
261
248
|
elif status_code == 401:
|
|
262
249
|
raise RuntimeError("认证失败 [401]: " + response.reason)
|
|
263
250
|
elif status_code == 403:
|
ddns/provider/alidns.py
CHANGED
|
@@ -5,9 +5,10 @@ AliDNS API
|
|
|
5
5
|
@author: NewFuture
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from time import gmtime, strftime, time
|
|
9
|
+
|
|
10
|
+
from ._base import TYPE_FORM, BaseProvider, encode_params, join_domain
|
|
9
11
|
from ._signature import hmac_sha256_authorization, sha256_hash
|
|
10
|
-
from time import strftime, gmtime, time
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class AliBaseProvider(BaseProvider):
|
|
@@ -116,7 +117,7 @@ class AlidnsProvider(AliBaseProvider):
|
|
|
116
117
|
TTL=ttl,
|
|
117
118
|
Line=line,
|
|
118
119
|
**extra
|
|
119
|
-
)
|
|
120
|
+
) # fmt: skip
|
|
120
121
|
if data and data.get("RecordId"):
|
|
121
122
|
self.logger.info("Record created: %s", data)
|
|
122
123
|
return True
|
|
@@ -143,7 +144,7 @@ class AlidnsProvider(AliBaseProvider):
|
|
|
143
144
|
TTL=ttl,
|
|
144
145
|
Line=line or old_record.get("Line"),
|
|
145
146
|
**extra
|
|
146
|
-
)
|
|
147
|
+
) # fmt: skip
|
|
147
148
|
if data and data.get("RecordId"):
|
|
148
149
|
self.logger.info("Record updated: %s", data)
|
|
149
150
|
return True
|
ddns/provider/aliesa.py
CHANGED
|
@@ -5,10 +5,11 @@ AliESA API
|
|
|
5
5
|
@author: NewFuture, GitHub Copilot
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .alidns import AliBaseProvider
|
|
9
|
-
from ._base import join_domain, TYPE_JSON
|
|
10
8
|
from time import strftime
|
|
11
9
|
|
|
10
|
+
from ._base import TYPE_JSON, join_domain
|
|
11
|
+
from .alidns import AliBaseProvider
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
class AliesaProvider(AliBaseProvider):
|
|
14
15
|
"""阿里云边缘安全加速(ESA) DNS Provider"""
|
|
@@ -82,7 +83,7 @@ class AliesaProvider(AliBaseProvider):
|
|
|
82
83
|
Data={"Value": value},
|
|
83
84
|
Ttl=ttl or 1,
|
|
84
85
|
**extra
|
|
85
|
-
)
|
|
86
|
+
) # fmt: skip
|
|
86
87
|
|
|
87
88
|
if data and data.get("RecordId"):
|
|
88
89
|
self.logger.info("Record created: %s", data)
|
|
@@ -115,7 +116,7 @@ class AliesaProvider(AliBaseProvider):
|
|
|
115
116
|
Data={"Value": value},
|
|
116
117
|
Ttl=ttl,
|
|
117
118
|
**extra
|
|
118
|
-
)
|
|
119
|
+
) # fmt: skip
|
|
119
120
|
|
|
120
121
|
if data and data.get("RecordId"):
|
|
121
122
|
self.logger.info("Record updated: %s", data)
|
ddns/provider/callback.py
CHANGED
ddns/provider/cloudflare.py
CHANGED
|
@@ -4,7 +4,7 @@ CloudFlare API
|
|
|
4
4
|
@author: TongYifan, NewFuture
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from ._base import
|
|
7
|
+
from ._base import TYPE_JSON, BaseProvider, join_domain
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class CloudflareProvider(BaseProvider):
|
|
@@ -92,7 +92,7 @@ class CloudflareProvider(BaseProvider):
|
|
|
92
92
|
content=value,
|
|
93
93
|
ttl=ttl,
|
|
94
94
|
**extra
|
|
95
|
-
)
|
|
95
|
+
) # fmt: skip
|
|
96
96
|
self.logger.debug("Record updated: %s", data)
|
|
97
97
|
if data:
|
|
98
98
|
return True
|
ddns/provider/debug.py
CHANGED
ddns/provider/dnscom.py
CHANGED
|
@@ -5,10 +5,11 @@ www.51dns.com (原dns.com)
|
|
|
5
5
|
@author: Bigjin<i@bigjin.com>, NewFuture
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from ._base import BaseProvider, TYPE_FORM, encode_params
|
|
9
8
|
from hashlib import md5
|
|
10
9
|
from time import time
|
|
11
10
|
|
|
11
|
+
from ._base import TYPE_FORM, BaseProvider, encode_params
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
class DnscomProvider(BaseProvider):
|
|
14
15
|
"""
|
|
@@ -82,7 +83,7 @@ class DnscomProvider(BaseProvider):
|
|
|
82
83
|
TTL=ttl,
|
|
83
84
|
viewID=line,
|
|
84
85
|
**extra
|
|
85
|
-
)
|
|
86
|
+
) # fmt: skip
|
|
86
87
|
if res and res.get("recordID"):
|
|
87
88
|
self.logger.info("Record created: %s", res)
|
|
88
89
|
return True
|
ddns/provider/dnspod.py
CHANGED
|
@@ -101,9 +101,7 @@ class DnspodProvider(BaseProvider):
|
|
|
101
101
|
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
102
102
|
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
103
103
|
"""查询记录 list 然后逐个查找 https://docs.dnspod.cn/api/record-list/"""
|
|
104
|
-
res = self._request(
|
|
105
|
-
"Record.List", domain_id=zone_id, sub_domain=subdomain, record_type=record_type, line=line
|
|
106
|
-
)
|
|
104
|
+
res = self._request("Record.List", domain_id=zone_id, sub_domain=subdomain, record_type=record_type, line=line)
|
|
107
105
|
# length="3000"
|
|
108
106
|
records = res.get("records", [])
|
|
109
107
|
n = len(records)
|
ddns/provider/edgeone.py
CHANGED
ddns/provider/huaweidns.py
CHANGED
|
@@ -5,9 +5,10 @@ HuaweiDNS API
|
|
|
5
5
|
@author: NewFuture
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from time import gmtime, strftime
|
|
9
|
+
|
|
10
|
+
from ._base import TYPE_JSON, BaseProvider, encode_params, join_domain
|
|
9
11
|
from ._signature import hmac_sha256_authorization, sha256_hash
|
|
10
|
-
from time import strftime, gmtime
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class HuaweiDNSProvider(BaseProvider):
|
|
@@ -114,7 +115,7 @@ class HuaweiDNSProvider(BaseProvider):
|
|
|
114
115
|
ttl=ttl,
|
|
115
116
|
line=line,
|
|
116
117
|
**extra
|
|
117
|
-
)
|
|
118
|
+
) # fmt: skip
|
|
118
119
|
if res and res.get("id"):
|
|
119
120
|
self.logger.info("Record created: %s", res)
|
|
120
121
|
return True
|
|
@@ -134,7 +135,7 @@ class HuaweiDNSProvider(BaseProvider):
|
|
|
134
135
|
records=[value],
|
|
135
136
|
ttl=ttl if ttl is not None else old_record.get("ttl"),
|
|
136
137
|
**extra
|
|
137
|
-
)
|
|
138
|
+
) # fmt: skip
|
|
138
139
|
if res and res.get("id"):
|
|
139
140
|
self.logger.info("Record updated: %s", res)
|
|
140
141
|
return True
|
ddns/provider/noip.py
CHANGED
|
@@ -4,8 +4,7 @@ No-IP (noip.com) Dynamic DNS API
|
|
|
4
4
|
@author: GitHub Copilot
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
from ._base import SimpleProvider, TYPE_FORM
|
|
7
|
+
from ._base import SimpleProvider, TYPE_FORM, quote
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class NoipProvider(SimpleProvider):
|
|
@@ -24,13 +23,23 @@ class NoipProvider(SimpleProvider):
|
|
|
24
23
|
|
|
25
24
|
def _validate(self):
|
|
26
25
|
"""
|
|
27
|
-
Validate authentication credentials for No-IP
|
|
26
|
+
Validate authentication credentials for No-IP and update endpoint with auth
|
|
28
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
|
+
|
|
29
32
|
if not self.id:
|
|
30
33
|
raise ValueError("No-IP requires username as 'id'")
|
|
31
34
|
if not self.token:
|
|
32
35
|
raise ValueError("No-IP requires password as 'token'")
|
|
33
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
|
+
|
|
34
43
|
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
|
|
35
44
|
"""
|
|
36
45
|
Update DNS record using No-IP Dynamic Update API
|
|
@@ -59,21 +68,10 @@ class NoipProvider(SimpleProvider):
|
|
|
59
68
|
# Prepare request parameters
|
|
60
69
|
params = {"hostname": domain, "myip": value}
|
|
61
70
|
|
|
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
71
|
try:
|
|
75
72
|
# Use GET request as it's the most common method for DDNS
|
|
76
|
-
|
|
73
|
+
# Endpoint already includes auth credentials from _validate()
|
|
74
|
+
response = self._http("GET", "/nic/update", queries=params)
|
|
77
75
|
|
|
78
76
|
if response is not None:
|
|
79
77
|
response_str = str(response).strip()
|
ddns/provider/tencentcloud.py
CHANGED
|
@@ -5,9 +5,11 @@ Tencent Cloud DNSPod API
|
|
|
5
5
|
|
|
6
6
|
@author: NewFuture
|
|
7
7
|
"""
|
|
8
|
-
|
|
9
|
-
from
|
|
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):
|
|
@@ -45,10 +47,7 @@ class TencentCloudProvider(BaseProvider):
|
|
|
45
47
|
body = self._encode_body(params)
|
|
46
48
|
|
|
47
49
|
# 构建请求头,小写 腾讯云只签名特定头部
|
|
48
|
-
headers = {
|
|
49
|
-
"content-type": self.content_type,
|
|
50
|
-
"host": self.endpoint.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())
|
|
@@ -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/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
|