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
ddns/__builtins__.pyi
ADDED
ddns/__init__.py
CHANGED
|
@@ -6,14 +6,7 @@ ddns Package
|
|
|
6
6
|
__description__ = "automatically update DNS records to my IP [域名自动指向本机IP]"
|
|
7
7
|
|
|
8
8
|
# 编译时,版本会被替换
|
|
9
|
-
__version__ = "4.0
|
|
9
|
+
__version__ = "4.1.0"
|
|
10
10
|
|
|
11
11
|
# 时间也会被替换掉
|
|
12
|
-
build_date = "2025-
|
|
13
|
-
|
|
14
|
-
__doc__ = """
|
|
15
|
-
ddns [v{}@{}]
|
|
16
|
-
(i) homepage or docs [文档主页]: https://ddns.newfuture.cc/
|
|
17
|
-
(?) issues or bugs [问题和反馈]: https://github.com/NewFuture/DDNS/issues
|
|
18
|
-
Copyright (c) New Future (MIT License)
|
|
19
|
-
""".format(__version__, build_date)
|
|
12
|
+
build_date = "2025-10-09T09:06:52Z"
|
ddns/__main__.py
CHANGED
|
@@ -1,187 +1,151 @@
|
|
|
1
1
|
# -*- coding:utf-8 -*-
|
|
2
2
|
"""
|
|
3
3
|
DDNS
|
|
4
|
-
@author:
|
|
5
|
-
@modified: rufengsuixing
|
|
4
|
+
@author: NewFuture, rufengsuixing
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
import sys
|
|
9
8
|
from io import TextIOWrapper
|
|
9
|
+
from logging import getLogger
|
|
10
10
|
from subprocess import check_output
|
|
11
|
-
from tempfile import gettempdir
|
|
12
|
-
from logging import basicConfig, info, warning, error, debug, INFO
|
|
13
|
-
|
|
14
|
-
import sys
|
|
15
|
-
|
|
16
|
-
from .__init__ import __version__, __description__, __doc__, build_date
|
|
17
|
-
from .util import ip
|
|
18
|
-
from .util.cache import Cache
|
|
19
|
-
from .util.config import init_config, get_config
|
|
20
11
|
|
|
21
|
-
|
|
12
|
+
from . import ip
|
|
13
|
+
from .__init__ import __description__, __version__, build_date
|
|
14
|
+
from .cache import Cache
|
|
15
|
+
from .config import Config, load_configs # noqa: F401
|
|
16
|
+
from .provider import SimpleProvider, get_provider_class # noqa: F401
|
|
22
17
|
|
|
18
|
+
logger = getLogger()
|
|
23
19
|
|
|
24
|
-
def is_false(value):
|
|
25
|
-
"""
|
|
26
|
-
判断值是否为 False
|
|
27
|
-
字符串 'false', 或者 False, 或者 'none';
|
|
28
|
-
0 不是 False
|
|
29
|
-
"""
|
|
30
|
-
if isinstance(value, str):
|
|
31
|
-
return value.strip().lower() in ['false', 'none']
|
|
32
|
-
return value is False
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
def get_ip(ip_type, index="default"):
|
|
21
|
+
def get_ip(ip_type, rules):
|
|
36
22
|
"""
|
|
37
23
|
get IP address
|
|
38
24
|
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try:
|
|
43
|
-
debug("get_ip(%s, %s)", ip_type, index)
|
|
44
|
-
if is_false(index): # disabled
|
|
45
|
-
return False
|
|
46
|
-
elif isinstance(index, list): # 如果获取到的规则是列表,则依次判断列表中每一个规则,直到获取到IP
|
|
47
|
-
for i in index:
|
|
48
|
-
value = get_ip(ip_type, i)
|
|
49
|
-
if value:
|
|
50
|
-
break
|
|
51
|
-
elif str(index).isdigit(): # 数字 local eth
|
|
52
|
-
value = getattr(ip, "local_v" + ip_type)(index)
|
|
53
|
-
elif index.startswith('cmd:'): # cmd
|
|
54
|
-
value = str(check_output(index[4:]).strip().decode('utf-8'))
|
|
55
|
-
elif index.startswith('shell:'): # shell
|
|
56
|
-
value = str(check_output(
|
|
57
|
-
index[6:], shell=True).strip().decode('utf-8'))
|
|
58
|
-
elif index.startswith('url:'): # 自定义 url
|
|
59
|
-
value = getattr(ip, "public_v" + ip_type)(index[4:])
|
|
60
|
-
elif index.startswith('regex:'): # 正则 regex
|
|
61
|
-
value = getattr(ip, "regex_v" + ip_type)(index[6:])
|
|
62
|
-
else:
|
|
63
|
-
value = getattr(ip, index + "_v" + ip_type)()
|
|
64
|
-
except Exception as e:
|
|
65
|
-
error("Failed to get %s address: %s", ip_type, e)
|
|
66
|
-
return value
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def change_dns_record(dns, proxy_list, **kw):
|
|
70
|
-
for proxy in proxy_list:
|
|
71
|
-
if not proxy or (proxy.upper() in ['DIRECT', 'NONE']):
|
|
72
|
-
dns.Config.PROXY = None
|
|
73
|
-
else:
|
|
74
|
-
dns.Config.PROXY = proxy
|
|
75
|
-
record_type, domain = kw['record_type'], kw['domain']
|
|
76
|
-
info("%s(%s) ==> %s [via %s]", domain, record_type, kw['ip'], proxy)
|
|
25
|
+
if rules is False: # disabled
|
|
26
|
+
return False
|
|
27
|
+
for i in rules:
|
|
77
28
|
try:
|
|
78
|
-
|
|
29
|
+
logger.debug("get_ip:(%s, %s)", ip_type, i)
|
|
30
|
+
if str(i).isdigit(): # 数字 local eth
|
|
31
|
+
return getattr(ip, "local_v" + ip_type)(i)
|
|
32
|
+
elif i.startswith("cmd:"): # cmd
|
|
33
|
+
return str(check_output(i[4:]).strip().decode("utf-8"))
|
|
34
|
+
elif i.startswith("shell:"): # shell
|
|
35
|
+
return str(check_output(i[6:], shell=True).strip().decode("utf-8"))
|
|
36
|
+
elif i.startswith("url:"): # 自定义 url
|
|
37
|
+
return getattr(ip, "public_v" + ip_type)(i[4:])
|
|
38
|
+
elif i.startswith("regex:"): # 正则 regex
|
|
39
|
+
return getattr(ip, "regex_v" + ip_type)(i[6:])
|
|
40
|
+
else:
|
|
41
|
+
return getattr(ip, i + "_v" + ip_type)()
|
|
79
42
|
except Exception as e:
|
|
80
|
-
error("Failed to
|
|
81
|
-
return
|
|
43
|
+
logger.error("Failed to get %s address: %s", ip_type, e)
|
|
44
|
+
return None
|
|
82
45
|
|
|
83
46
|
|
|
84
|
-
def update_ip(
|
|
47
|
+
def update_ip(dns, cache, index_rule, domains, record_type, config):
|
|
48
|
+
# type: (SimpleProvider, Cache | None, list[str]|bool, list[str], str, Config) -> bool | None
|
|
85
49
|
"""
|
|
86
|
-
更新IP
|
|
50
|
+
更新IP并变更DNS记录
|
|
87
51
|
"""
|
|
88
|
-
ipname = 'ipv' + ip_type
|
|
89
|
-
domains = get_config(ipname)
|
|
90
52
|
if not domains:
|
|
91
53
|
return None
|
|
92
|
-
if not isinstance(domains, list):
|
|
93
|
-
domains = domains.strip('; ').replace(',', ';').replace(' ', ';').split(';')
|
|
94
54
|
|
|
95
|
-
|
|
55
|
+
ip_type = "4" if record_type == "A" else "6"
|
|
96
56
|
address = get_ip(ip_type, index_rule)
|
|
97
57
|
if not address:
|
|
98
|
-
error(
|
|
58
|
+
logger.error("Fail to get %s address!", ip_type)
|
|
99
59
|
return False
|
|
100
60
|
|
|
101
|
-
if cache and (address == cache.get(ipname)):
|
|
102
|
-
info('%s address not changed, using cache.', ipname)
|
|
103
|
-
return True
|
|
104
|
-
|
|
105
|
-
record_type = 'A' if ip_type == '4' else 'AAAA'
|
|
106
61
|
update_success = False
|
|
62
|
+
|
|
107
63
|
for domain in domains:
|
|
108
64
|
domain = domain.lower()
|
|
109
|
-
|
|
65
|
+
cache_key = "{}:{}".format(domain, record_type)
|
|
66
|
+
if cache and cache.get(cache_key) == address:
|
|
67
|
+
logger.info("%s[%s] address not changed, using cache: %s", domain, record_type, address)
|
|
110
68
|
update_success = True
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
69
|
+
else:
|
|
70
|
+
try:
|
|
71
|
+
result = dns.set_record(domain, address, record_type=record_type, ttl=config.ttl, line=config.line)
|
|
72
|
+
if result:
|
|
73
|
+
logger.warning("set %s[IPv%s]: %s successfully.", domain, ip_type, address)
|
|
74
|
+
update_success = True
|
|
75
|
+
if isinstance(cache, dict):
|
|
76
|
+
cache[cache_key] = address
|
|
77
|
+
else:
|
|
78
|
+
logger.error("Failed to update %s record for %s", record_type, domain)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.exception("Failed to update %s record for %s: %s", record_type, domain, e)
|
|
115
81
|
return update_success
|
|
116
82
|
|
|
117
83
|
|
|
118
|
-
def
|
|
84
|
+
def run(config):
|
|
85
|
+
# type: (Config) -> bool
|
|
119
86
|
"""
|
|
120
|
-
|
|
87
|
+
Run the DDNS update process
|
|
121
88
|
"""
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
elif log_level < INFO:
|
|
135
|
-
# Override log format in debug mode to include filename and line number for detailed debugging
|
|
136
|
-
log_format = '%(asctime)s %(levelname)s [%(module)s.%(funcName)s](%(filename)s:%(lineno)d): %(message)s'
|
|
137
|
-
elif log_level > INFO:
|
|
138
|
-
log_format = '%(asctime)s %(levelname)s: %(message)s'
|
|
139
|
-
else:
|
|
140
|
-
log_format = '%(asctime)s %(levelname)s [%(module)s]: %(message)s'
|
|
141
|
-
basicConfig(
|
|
142
|
-
level=log_level,
|
|
143
|
-
format=log_format,
|
|
144
|
-
datefmt=get_config('log.datefmt', '%Y-%m-%dT%H:%M:%S'),
|
|
145
|
-
filename=get_config('log.file'),
|
|
89
|
+
# 设置IP模块的SSL验证配置
|
|
90
|
+
ip.ssl_verify = config.ssl
|
|
91
|
+
|
|
92
|
+
# dns provider class
|
|
93
|
+
provider_class = get_provider_class(config.dns)
|
|
94
|
+
dns = provider_class(
|
|
95
|
+
config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy, ssl=config.ssl
|
|
96
|
+
)
|
|
97
|
+
cache = Cache.new(config.cache, config.md5(), logger)
|
|
98
|
+
return (
|
|
99
|
+
update_ip(dns, cache, config.index4, config.ipv4, "A", config) is not False
|
|
100
|
+
and update_ip(dns, cache, config.index6, config.ipv6, "AAAA", config) is not False
|
|
146
101
|
)
|
|
147
102
|
|
|
148
|
-
info("DDNS[ %s ] run: %s %s", __version__, os_name, sys.platform)
|
|
149
|
-
|
|
150
|
-
# Dynamically import the dns module as configuration
|
|
151
|
-
dns_provider = str(get_config('dns', 'dnspod').lower())
|
|
152
|
-
# dns_module = __import__(
|
|
153
|
-
# '.dns', fromlist=[dns_provider], package=__package__)
|
|
154
|
-
dns = getattr(__import__('ddns.provider', fromlist=[dns_provider]), dns_provider)
|
|
155
|
-
# dns = getattr(dns_module, dns_provider)
|
|
156
|
-
dns.Config.ID = get_config('id')
|
|
157
|
-
dns.Config.TOKEN = get_config('token')
|
|
158
|
-
dns.Config.TTL = get_config('ttl')
|
|
159
|
-
|
|
160
|
-
if get_config("config"):
|
|
161
|
-
info('loaded Config from: %s', path.abspath(get_config('config')))
|
|
162
|
-
|
|
163
|
-
proxy = get_config('proxy') or 'DIRECT'
|
|
164
|
-
proxy_list = proxy if isinstance(
|
|
165
|
-
proxy, list) else proxy.strip(';').replace(',', ';').split(';')
|
|
166
|
-
|
|
167
|
-
cache_config = get_config('cache', True)
|
|
168
|
-
if cache_config is False:
|
|
169
|
-
cache = cache_config
|
|
170
|
-
elif cache_config is True:
|
|
171
|
-
cache = Cache(path.join(gettempdir(), 'ddns.cache'))
|
|
172
|
-
else:
|
|
173
|
-
cache = Cache(cache_config)
|
|
174
103
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
104
|
+
def main():
|
|
105
|
+
stdout = sys.stdout # pythonw 模式无 stdout
|
|
106
|
+
if stdout and stdout.encoding and stdout.encoding.lower() != "utf-8" and hasattr(stdout, "buffer"):
|
|
107
|
+
# 兼容windows 和部分ASCII编码的老旧系统
|
|
108
|
+
sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
109
|
+
sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
# Windows 下输出一个空行
|
|
112
|
+
if stdout and sys.platform.startswith("win"):
|
|
113
|
+
stdout.write("\r\n")
|
|
114
|
+
|
|
115
|
+
logger.name = "ddns"
|
|
116
|
+
|
|
117
|
+
# 使用多配置加载器,它会自动处理单个和多个配置
|
|
118
|
+
configs = load_configs(__description__, __version__, build_date)
|
|
119
|
+
|
|
120
|
+
if len(configs) == 1:
|
|
121
|
+
# 单个配置,使用原有逻辑(向后兼容)
|
|
122
|
+
config = configs[0]
|
|
123
|
+
success = run(config)
|
|
124
|
+
if not success:
|
|
125
|
+
sys.exit(1)
|
|
180
126
|
else:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
127
|
+
# 多个配置,使用新的批处理逻辑
|
|
128
|
+
overall_success = True
|
|
129
|
+
for i, config in enumerate(configs):
|
|
130
|
+
# 如果log_level有值则设置setLevel
|
|
131
|
+
if hasattr(config, "log_level") and config.log_level:
|
|
132
|
+
logger.setLevel(config.log_level)
|
|
133
|
+
logger.info("Running configuration %d/%d", i + 1, len(configs))
|
|
134
|
+
# 记录当前provider
|
|
135
|
+
logger.info("Using DNS provider: %s", config.dns)
|
|
136
|
+
success = run(config)
|
|
137
|
+
if not success:
|
|
138
|
+
overall_success = False
|
|
139
|
+
logger.error("Configuration %d failed", i + 1)
|
|
140
|
+
else:
|
|
141
|
+
logger.info("Configuration %d completed successfully", i + 1)
|
|
142
|
+
|
|
143
|
+
if not overall_success:
|
|
144
|
+
logger.error("Some configurations failed")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
else:
|
|
147
|
+
logger.info("All configurations completed successfully")
|
|
184
148
|
|
|
185
149
|
|
|
186
|
-
if __name__ ==
|
|
150
|
+
if __name__ == "__main__":
|
|
187
151
|
main()
|
ddns/cache.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
cache module
|
|
4
|
+
文件缓存
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from logging import getLogger, Logger # noqa: F401
|
|
8
|
+
from os import path, stat
|
|
9
|
+
from json import load, dump
|
|
10
|
+
from tempfile import gettempdir
|
|
11
|
+
from time import time
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Cache(dict):
|
|
15
|
+
"""
|
|
16
|
+
using file to Cache data as dictionary
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, path, logger=None, sync=False):
|
|
20
|
+
# type: (str, Logger | None, bool) -> None
|
|
21
|
+
super(Cache, self).__init__()
|
|
22
|
+
self.__filename = path
|
|
23
|
+
self.__sync = sync
|
|
24
|
+
self.__time = time()
|
|
25
|
+
self.__changed = False
|
|
26
|
+
self.__logger = (logger or getLogger()).getChild("Cache")
|
|
27
|
+
self.load()
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def time(self):
|
|
31
|
+
"""
|
|
32
|
+
缓存修改时间
|
|
33
|
+
"""
|
|
34
|
+
return self.__time or 0
|
|
35
|
+
|
|
36
|
+
def load(self, file=None):
|
|
37
|
+
"""
|
|
38
|
+
load data from path
|
|
39
|
+
"""
|
|
40
|
+
if not file:
|
|
41
|
+
file = self.__filename
|
|
42
|
+
|
|
43
|
+
self.__logger.debug("load cache data from %s", file)
|
|
44
|
+
if file:
|
|
45
|
+
try:
|
|
46
|
+
with open(file, "r") as data:
|
|
47
|
+
loaded_data = load(data)
|
|
48
|
+
self.clear()
|
|
49
|
+
self.update(loaded_data)
|
|
50
|
+
self.__time = stat(file).st_mtime
|
|
51
|
+
return self
|
|
52
|
+
except (IOError, OSError):
|
|
53
|
+
self.__logger.info("cache file not exist or cannot be opened")
|
|
54
|
+
except ValueError:
|
|
55
|
+
pass
|
|
56
|
+
except Exception as e:
|
|
57
|
+
self.__logger.warning(e)
|
|
58
|
+
else:
|
|
59
|
+
self.__logger.info("cache file not exist")
|
|
60
|
+
|
|
61
|
+
self.clear()
|
|
62
|
+
self.__time = time()
|
|
63
|
+
self.__changed = True
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def sync(self):
|
|
67
|
+
"""Sync the write buffer with the cache files and clear the buffer."""
|
|
68
|
+
if self.__changed and self.__filename:
|
|
69
|
+
with open(self.__filename, "w") as data:
|
|
70
|
+
# 只保存非私有字段(不以__开头的字段)
|
|
71
|
+
filtered_data = {k: v for k, v in super(Cache, self).items() if not k.startswith("__")}
|
|
72
|
+
dump(filtered_data, data, separators=(",", ":"))
|
|
73
|
+
self.__logger.debug("save cache data to %s", self.__filename)
|
|
74
|
+
self.__time = time()
|
|
75
|
+
self.__changed = False
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def close(self):
|
|
79
|
+
"""Sync the write buffer, then close the cache.
|
|
80
|
+
If a closed :class:`FileCache` object's methods are called, a
|
|
81
|
+
:exc:`ValueError` will be raised.
|
|
82
|
+
"""
|
|
83
|
+
if self.__filename:
|
|
84
|
+
self.sync()
|
|
85
|
+
self.__filename = None
|
|
86
|
+
self.__time = None
|
|
87
|
+
self.__sync = False
|
|
88
|
+
|
|
89
|
+
def __update(self):
|
|
90
|
+
self.__changed = True
|
|
91
|
+
if self.__sync:
|
|
92
|
+
self.sync()
|
|
93
|
+
else:
|
|
94
|
+
self.__time = time()
|
|
95
|
+
|
|
96
|
+
def clear(self):
|
|
97
|
+
# 只清除非私有字段(不以__开头的字段)
|
|
98
|
+
keys_to_remove = [key for key in super(Cache, self).keys() if not key.startswith("__")]
|
|
99
|
+
if keys_to_remove:
|
|
100
|
+
for key in keys_to_remove:
|
|
101
|
+
super(Cache, self).__delitem__(key)
|
|
102
|
+
self.__update()
|
|
103
|
+
|
|
104
|
+
def get(self, key, default=None):
|
|
105
|
+
"""
|
|
106
|
+
获取指定键的值,如果键不存在则返回默认值
|
|
107
|
+
:param key: 键
|
|
108
|
+
:param default: 默认值
|
|
109
|
+
:return: 键对应的值或默认值
|
|
110
|
+
"""
|
|
111
|
+
if key is None and default is None:
|
|
112
|
+
return {k: v for k, v in super(Cache, self).items() if not k.startswith("__")}
|
|
113
|
+
return super(Cache, self).get(key, default)
|
|
114
|
+
|
|
115
|
+
def __setitem__(self, key, value):
|
|
116
|
+
if self.get(key) != value:
|
|
117
|
+
super(Cache, self).__setitem__(key, value)
|
|
118
|
+
# 私有字段(以__开头)不触发同步
|
|
119
|
+
if not key.startswith("__"):
|
|
120
|
+
self.__update()
|
|
121
|
+
|
|
122
|
+
def __delitem__(self, key):
|
|
123
|
+
# 检查键是否存在,如果不存在则直接返回,不抛错
|
|
124
|
+
if not super(Cache, self).__contains__(key):
|
|
125
|
+
return
|
|
126
|
+
super(Cache, self).__delitem__(key)
|
|
127
|
+
# 私有字段(以__开头)不触发同步
|
|
128
|
+
if not key.startswith("__"):
|
|
129
|
+
self.__update()
|
|
130
|
+
|
|
131
|
+
def __getitem__(self, key):
|
|
132
|
+
return super(Cache, self).__getitem__(key)
|
|
133
|
+
|
|
134
|
+
def __iter__(self):
|
|
135
|
+
# 只迭代非私有字段(不以__开头的字段)
|
|
136
|
+
for key in super(Cache, self).__iter__():
|
|
137
|
+
if not key.startswith("__"):
|
|
138
|
+
yield key
|
|
139
|
+
|
|
140
|
+
def __items__(self):
|
|
141
|
+
# 只返回非私有字段(不以__开头的字段)
|
|
142
|
+
return ((key, value) for key, value in super(Cache, self).items() if not key.startswith("__"))
|
|
143
|
+
|
|
144
|
+
def __len__(self):
|
|
145
|
+
# 不计算以__开头的私有字段
|
|
146
|
+
return len([key for key in super(Cache, self).keys() if not key.startswith("__")])
|
|
147
|
+
|
|
148
|
+
def __contains__(self, key):
|
|
149
|
+
return super(Cache, self).__contains__(key)
|
|
150
|
+
|
|
151
|
+
def __str__(self):
|
|
152
|
+
return super(Cache, self).__str__()
|
|
153
|
+
|
|
154
|
+
def __del__(self):
|
|
155
|
+
self.close()
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def new(config_cache, hash, logger):
|
|
159
|
+
# type: (str|bool, str, Logger) -> Cache|None
|
|
160
|
+
"""
|
|
161
|
+
new cache from a file path.
|
|
162
|
+
:param path: Path to the cache file.
|
|
163
|
+
:param logger: Optional logger for debug messages.
|
|
164
|
+
:return: Cache instance with loaded data.
|
|
165
|
+
"""
|
|
166
|
+
if config_cache is False:
|
|
167
|
+
cache = None
|
|
168
|
+
elif config_cache is True:
|
|
169
|
+
cache_path = path.join(gettempdir(), "ddns.%s.cache" % hash)
|
|
170
|
+
cache = Cache(cache_path, logger)
|
|
171
|
+
else:
|
|
172
|
+
cache = Cache(config_cache, logger)
|
|
173
|
+
|
|
174
|
+
if cache is None:
|
|
175
|
+
logger.debug("Cache is disabled!")
|
|
176
|
+
elif cache.time + 72 * 3600 < time(): # 72小时有效期
|
|
177
|
+
logger.info("Cache file is outdated.")
|
|
178
|
+
cache.clear()
|
|
179
|
+
elif len(cache) == 0:
|
|
180
|
+
logger.debug("Cache is empty.")
|
|
181
|
+
else:
|
|
182
|
+
logger.debug("Cache loaded with %d entries.", len(cache))
|
|
183
|
+
return cache
|
ddns/ip.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding:utf-8 -*-
|
|
3
|
+
from re import compile
|
|
4
|
+
from os import name as os_name, popen
|
|
5
|
+
from socket import socket, getaddrinfo, gethostname, AF_INET, AF_INET6, SOCK_DGRAM
|
|
6
|
+
from logging import debug, error
|
|
7
|
+
|
|
8
|
+
from .util.http import request
|
|
9
|
+
|
|
10
|
+
# 模块级别的SSL验证配置,默认使用auto模式
|
|
11
|
+
ssl_verify = "auto"
|
|
12
|
+
|
|
13
|
+
# IPV4正则
|
|
14
|
+
IPV4_REG = r"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"
|
|
15
|
+
# IPV6正则
|
|
16
|
+
# https://community.helpsystems.com/forums/intermapper/miscellaneous-topics/5acc4fcf-fa83-e511-80cf-0050568460e4
|
|
17
|
+
IPV6_REG = r"((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))" # noqa: E501
|
|
18
|
+
|
|
19
|
+
# 公网IPv4 API列表,按优先级排序
|
|
20
|
+
PUBLIC_IPV4_APIS = [
|
|
21
|
+
"https://api.ipify.cn",
|
|
22
|
+
"https://api.ipify.org",
|
|
23
|
+
"https://4.ipw.cn/",
|
|
24
|
+
"https://ipinfo.io/ip",
|
|
25
|
+
"https://api-ipv4.ip.sb/ip",
|
|
26
|
+
"http://checkip.amazonaws.com",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# 公网IPv6 API列表,按优先级排序
|
|
30
|
+
PUBLIC_IPV6_APIS = [
|
|
31
|
+
"https://api6.ipify.org/",
|
|
32
|
+
"https://6.ipw.cn/",
|
|
33
|
+
"https://api-ipv6.ip.sb/ip",
|
|
34
|
+
"http://ipv6.icanhazip.com",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_v4(): # 默认连接外网的ipv4
|
|
39
|
+
s = socket(AF_INET, SOCK_DGRAM)
|
|
40
|
+
s.connect(("1.1.1.1", 53))
|
|
41
|
+
ip = s.getsockname()[0]
|
|
42
|
+
s.close()
|
|
43
|
+
return ip
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def default_v6(): # 默认连接外网的ipv6
|
|
47
|
+
s = socket(AF_INET6, SOCK_DGRAM)
|
|
48
|
+
s.connect(("1:1:1:1:1:1:1:1", 8))
|
|
49
|
+
ip = s.getsockname()[0]
|
|
50
|
+
s.close()
|
|
51
|
+
return ip
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def local_v6(i=0): # 本地ipv6地址
|
|
55
|
+
info = getaddrinfo(gethostname(), 0, AF_INET6)
|
|
56
|
+
debug(info)
|
|
57
|
+
return info[int(i)][4][0]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def local_v4(i=0): # 本地ipv4地址
|
|
61
|
+
info = getaddrinfo(gethostname(), 0, AF_INET)
|
|
62
|
+
debug(info)
|
|
63
|
+
return info[int(i)][-1][0]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _open(url, reg):
|
|
67
|
+
try:
|
|
68
|
+
debug("open: %s", url)
|
|
69
|
+
# IP 模块重试3次
|
|
70
|
+
response = request("GET", url, verify=ssl_verify, retries=2)
|
|
71
|
+
res = response.body
|
|
72
|
+
debug("response: %s", res)
|
|
73
|
+
match = compile(reg).search(res)
|
|
74
|
+
if match:
|
|
75
|
+
return match.group()
|
|
76
|
+
error("No match found in response: %s", res)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
error(e)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _try_multiple_apis(api_list, reg, ip_type):
|
|
82
|
+
"""
|
|
83
|
+
Try multiple API endpoints until one succeeds
|
|
84
|
+
"""
|
|
85
|
+
for url in api_list:
|
|
86
|
+
try:
|
|
87
|
+
debug("Trying %s API: %s", ip_type, url)
|
|
88
|
+
result = _open(url, reg)
|
|
89
|
+
if result:
|
|
90
|
+
debug("Successfully got %s from %s: %s", ip_type, url, result)
|
|
91
|
+
return result
|
|
92
|
+
else:
|
|
93
|
+
debug("No valid IP found from %s", url)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
debug("Failed to get %s from %s: %s", ip_type, url, e)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def public_v4(url=None, reg=IPV4_REG): # 公网IPV4地址
|
|
100
|
+
if url:
|
|
101
|
+
# 使用指定URL
|
|
102
|
+
return _open(url, reg)
|
|
103
|
+
else:
|
|
104
|
+
# 使用多个API自动重试
|
|
105
|
+
return _try_multiple_apis(PUBLIC_IPV4_APIS, reg, "IPv4")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def public_v6(url=None, reg=IPV6_REG): # 公网IPV6地址
|
|
109
|
+
if url:
|
|
110
|
+
# 使用指定URL
|
|
111
|
+
return _open(url, reg)
|
|
112
|
+
else:
|
|
113
|
+
# 使用多个API自动重试
|
|
114
|
+
return _try_multiple_apis(PUBLIC_IPV6_APIS, reg, "IPv6")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _ip_regex_match(parrent_regex, match_regex):
|
|
118
|
+
ip_pattern = compile(parrent_regex)
|
|
119
|
+
matcher = compile(match_regex)
|
|
120
|
+
|
|
121
|
+
if os_name == "nt": # windows:
|
|
122
|
+
cmd = "ipconfig"
|
|
123
|
+
else:
|
|
124
|
+
cmd = "ip address || ifconfig 2>/dev/null"
|
|
125
|
+
|
|
126
|
+
for s in popen(cmd).readlines():
|
|
127
|
+
addr = ip_pattern.search(s)
|
|
128
|
+
if addr and matcher.match(addr.group(1)):
|
|
129
|
+
return addr.group(1)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def regex_v4(reg): # ipv4 正则提取
|
|
133
|
+
if os_name == "nt": # Windows: IPv4 xxx: 192.168.1.2
|
|
134
|
+
regex_str = r"IPv4 .*: ((?:\d{1,3}\.){3}\d{1,3})\W"
|
|
135
|
+
else:
|
|
136
|
+
regex_str = r"inet (?:addr\:)?((?:\d{1,3}\.){3}\d{1,3})[\s/]"
|
|
137
|
+
return _ip_regex_match(regex_str, reg)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def regex_v6(reg): # ipv6 正则提取
|
|
141
|
+
if os_name == "nt": # Windows: IPv4 xxx: ::1
|
|
142
|
+
regex_str = r"IPv6 .*: ([\:\dabcdef]*)?\W"
|
|
143
|
+
else:
|
|
144
|
+
regex_str = r"inet6 (?:addr\:\s*)?([\:\dabcdef]*)?[\s/%]"
|
|
145
|
+
return _ip_regex_match(regex_str, reg)
|