ddns 4.0.2__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/__builtins__.pyi +5 -0
- ddns/__init__.py +2 -9
- ddns/__main__.py +77 -146
- ddns/cache.py +183 -0
- ddns/{util/ip.py → ip.py} +26 -21
- ddns/provider/__init__.py +79 -0
- ddns/provider/_base.py +539 -0
- ddns/provider/_signature.py +113 -0
- ddns/provider/alidns.py +139 -171
- ddns/provider/aliesa.py +129 -0
- ddns/provider/callback.py +66 -105
- ddns/provider/cloudflare.py +87 -151
- ddns/provider/debug.py +20 -0
- ddns/provider/dnscom.py +91 -169
- ddns/provider/dnspod.py +104 -174
- ddns/provider/dnspod_com.py +11 -8
- ddns/provider/edgeone.py +82 -0
- ddns/provider/he.py +41 -78
- ddns/provider/huaweidns.py +133 -256
- ddns/provider/namesilo.py +159 -0
- ddns/provider/noip.py +103 -0
- ddns/provider/tencentcloud.py +195 -0
- ddns/util/comment.py +88 -0
- ddns/util/http.py +228 -0
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/METADATA +109 -75
- ddns-4.1.0b2.dist-info/RECORD +31 -0
- ddns/util/cache.py +0 -139
- ddns/util/config.py +0 -317
- ddns-4.0.2.dist-info/RECORD +0 -21
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/WHEEL +0 -0
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/entry_points.txt +0 -0
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.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.
|
|
9
|
+
__version__ = "4.1.0b2"
|
|
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-07-15T02:27:02Z"
|
ddns/__main__.py
CHANGED
|
@@ -1,187 +1,118 @@
|
|
|
1
1
|
# -*- coding:utf-8 -*-
|
|
2
2
|
"""
|
|
3
3
|
DDNS
|
|
4
|
-
@author:
|
|
5
|
-
@modified: rufengsuixing
|
|
4
|
+
@author: NewFuture, rufengsuixing
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
|
-
from os import path, environ, name as os_name
|
|
9
7
|
from io import TextIOWrapper
|
|
10
8
|
from subprocess import check_output
|
|
11
|
-
from
|
|
12
|
-
from logging import basicConfig, info, warning, error, debug, INFO
|
|
13
|
-
|
|
9
|
+
from logging import getLogger
|
|
14
10
|
import sys
|
|
15
11
|
|
|
16
|
-
from .__init__ import __version__, __description__,
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
20
|
-
|
|
21
|
-
environ["DDNS_VERSION"] = __version__
|
|
22
|
-
|
|
12
|
+
from .__init__ import __version__, __description__, build_date
|
|
13
|
+
from .config import load_config, Config # noqa: F401
|
|
14
|
+
from .provider import get_provider_class, SimpleProvider
|
|
15
|
+
from . import ip
|
|
16
|
+
from .cache import Cache
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
18
|
+
logger = getLogger()
|
|
19
|
+
# Set user agent for All Providers
|
|
20
|
+
SimpleProvider.user_agent = SimpleProvider.user_agent.format(version=__version__)
|
|
33
21
|
|
|
34
22
|
|
|
35
|
-
def get_ip(ip_type,
|
|
23
|
+
def get_ip(ip_type, rules):
|
|
36
24
|
"""
|
|
37
25
|
get IP address
|
|
38
26
|
"""
|
|
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)
|
|
27
|
+
if rules is False: # disabled
|
|
28
|
+
return False
|
|
29
|
+
for i in rules:
|
|
77
30
|
try:
|
|
78
|
-
|
|
31
|
+
logger.debug("get_ip:(%s, %s)", ip_type, i)
|
|
32
|
+
if str(i).isdigit(): # 数字 local eth
|
|
33
|
+
return getattr(ip, "local_v" + ip_type)(i)
|
|
34
|
+
elif i.startswith("cmd:"): # cmd
|
|
35
|
+
return str(check_output(i[4:]).strip().decode("utf-8"))
|
|
36
|
+
elif i.startswith("shell:"): # shell
|
|
37
|
+
return str(check_output(i[6:], shell=True).strip().decode("utf-8"))
|
|
38
|
+
elif i.startswith("url:"): # 自定义 url
|
|
39
|
+
return getattr(ip, "public_v" + ip_type)(i[4:])
|
|
40
|
+
elif i.startswith("regex:"): # 正则 regex
|
|
41
|
+
return getattr(ip, "regex_v" + ip_type)(i[6:])
|
|
42
|
+
else:
|
|
43
|
+
return getattr(ip, i + "_v" + ip_type)()
|
|
79
44
|
except Exception as e:
|
|
80
|
-
error("Failed to
|
|
81
|
-
return
|
|
45
|
+
logger.error("Failed to get %s address: %s", ip_type, e)
|
|
46
|
+
return None
|
|
82
47
|
|
|
83
48
|
|
|
84
|
-
def update_ip(
|
|
49
|
+
def update_ip(dns, cache, index_rule, domains, record_type, config):
|
|
50
|
+
# type: (SimpleProvider, Cache | None, list[str]|bool, list[str], str, Config) -> bool | None
|
|
85
51
|
"""
|
|
86
|
-
更新IP
|
|
52
|
+
更新IP并变更DNS记录
|
|
87
53
|
"""
|
|
88
|
-
ipname = 'ipv' + ip_type
|
|
89
|
-
domains = get_config(ipname)
|
|
90
54
|
if not domains:
|
|
91
55
|
return None
|
|
92
|
-
if not isinstance(domains, list):
|
|
93
|
-
domains = domains.strip('; ').replace(',', ';').replace(' ', ';').split(';')
|
|
94
56
|
|
|
95
|
-
|
|
57
|
+
ip_type = "4" if record_type == "A" else "6"
|
|
96
58
|
address = get_ip(ip_type, index_rule)
|
|
97
59
|
if not address:
|
|
98
|
-
error(
|
|
60
|
+
logger.error("Fail to get %s address!", ip_type)
|
|
99
61
|
return False
|
|
100
62
|
|
|
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
63
|
update_success = False
|
|
64
|
+
|
|
107
65
|
for domain in domains:
|
|
108
66
|
domain = domain.lower()
|
|
109
|
-
|
|
67
|
+
cache_key = "{}:{}".format(domain, record_type)
|
|
68
|
+
if cache and cache.get(cache_key) == address:
|
|
69
|
+
logger.info("%s[%s] address not changed, using cache: %s", domain, record_type, address)
|
|
110
70
|
update_success = True
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
71
|
+
else:
|
|
72
|
+
try:
|
|
73
|
+
result = dns.set_record(domain, address, record_type=record_type, ttl=config.ttl, line=config.line)
|
|
74
|
+
if result:
|
|
75
|
+
logger.warning("set %s[IPv%s]: %s successfully.", domain, ip_type, address)
|
|
76
|
+
update_success = True
|
|
77
|
+
if isinstance(cache, dict):
|
|
78
|
+
cache[cache_key] = address
|
|
79
|
+
else:
|
|
80
|
+
logger.error("Failed to update %s record for %s", record_type, domain)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.exception("Failed to update %s record for %s: %s", record_type, domain, e)
|
|
115
83
|
return update_success
|
|
116
84
|
|
|
117
85
|
|
|
118
|
-
def
|
|
86
|
+
def run(config):
|
|
87
|
+
# type: (Config) -> bool
|
|
119
88
|
"""
|
|
120
|
-
|
|
89
|
+
Run the DDNS update process
|
|
121
90
|
"""
|
|
91
|
+
# 设置IP模块的SSL验证配置
|
|
92
|
+
ip.ssl_verify = config.ssl
|
|
93
|
+
|
|
94
|
+
# dns provider class
|
|
95
|
+
provider_class = get_provider_class(config.dns)
|
|
96
|
+
dns = provider_class(
|
|
97
|
+
config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy, verify_ssl=config.ssl
|
|
98
|
+
)
|
|
99
|
+
cache = Cache.new(config.cache, config.md5(), logger)
|
|
100
|
+
return (
|
|
101
|
+
update_ip(dns, cache, config.index4, config.ipv4, "A", config) is not False
|
|
102
|
+
and update_ip(dns, cache, config.index6, config.ipv6, "AAAA", config) is not False
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
122
107
|
encode = sys.stdout.encoding
|
|
123
|
-
if encode is not None and encode.lower() !=
|
|
108
|
+
if encode is not None and encode.lower() != "utf-8" and hasattr(sys.stdout, "buffer"):
|
|
124
109
|
# 兼容windows 和部分ASCII编码的老旧系统
|
|
125
|
-
sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding=
|
|
126
|
-
sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding=
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if log_format:
|
|
132
|
-
# A custom log format is already set; no further action is required.
|
|
133
|
-
pass
|
|
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'),
|
|
146
|
-
)
|
|
110
|
+
sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
111
|
+
sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
112
|
+
logger.name = "ddns"
|
|
113
|
+
config = load_config(__description__, __version__, build_date)
|
|
114
|
+
run(config)
|
|
115
|
+
|
|
147
116
|
|
|
148
|
-
|
|
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
|
-
|
|
175
|
-
if cache is False:
|
|
176
|
-
info('Cache is disabled!')
|
|
177
|
-
elif not get_config('config_modified_time') or get_config('config_modified_time') >= cache.time:
|
|
178
|
-
warning('Cache file is outdated.')
|
|
179
|
-
cache.clear()
|
|
180
|
-
else:
|
|
181
|
-
debug('Cache is empty.')
|
|
182
|
-
update_ip('4', cache, dns, proxy_list)
|
|
183
|
-
update_ip('6', cache, dns, proxy_list)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if __name__ == '__main__':
|
|
117
|
+
if __name__ == "__main__":
|
|
187
118
|
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/{util/ip.py → ip.py}
RENAMED
|
@@ -4,16 +4,17 @@ from re import compile
|
|
|
4
4
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
|
|
8
|
+
from .util.http import send_http_request
|
|
9
|
+
|
|
10
|
+
# 模块级别的SSL验证配置,默认使用auto模式
|
|
11
|
+
ssl_verify = "auto"
|
|
11
12
|
|
|
12
13
|
# IPV4正则
|
|
13
|
-
IPV4_REG = r
|
|
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])"
|
|
14
15
|
# IPV6正则
|
|
15
16
|
# https://community.helpsystems.com/forums/intermapper/miscellaneous-topics/5acc4fcf-fa83-e511-80cf-0050568460e4
|
|
16
|
-
IPV6_REG = r
|
|
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
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def default_v4(): # 默认连接外网的ipv4
|
|
@@ -26,7 +27,7 @@ def default_v4(): # 默认连接外网的ipv4
|
|
|
26
27
|
|
|
27
28
|
def default_v6(): # 默认连接外网的ipv6
|
|
28
29
|
s = socket(AF_INET6, SOCK_DGRAM)
|
|
29
|
-
s.connect((
|
|
30
|
+
s.connect(("1:1:1:1:1:1:1:1", 8))
|
|
30
31
|
ip = s.getsockname()[0]
|
|
31
32
|
s.close()
|
|
32
33
|
return ip
|
|
@@ -47,11 +48,15 @@ def local_v4(i=0): # 本地ipv4地址
|
|
|
47
48
|
def _open(url, reg):
|
|
48
49
|
try:
|
|
49
50
|
debug("open: %s", url)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
response = send_http_request(
|
|
52
|
+
method="GET", url=url, headers={"User-Agent": "Mozilla/5.0 ddns"}, verify_ssl=ssl_verify
|
|
53
|
+
)
|
|
54
|
+
res = response.body
|
|
55
|
+
debug("response: %s", res)
|
|
56
|
+
match = compile(reg).search(res)
|
|
57
|
+
if match:
|
|
58
|
+
return match.group()
|
|
59
|
+
error("No match found in response: %s", res)
|
|
55
60
|
except Exception as e:
|
|
56
61
|
error(e)
|
|
57
62
|
|
|
@@ -69,10 +74,10 @@ def _ip_regex_match(parrent_regex, match_regex):
|
|
|
69
74
|
ip_pattern = compile(parrent_regex)
|
|
70
75
|
matcher = compile(match_regex)
|
|
71
76
|
|
|
72
|
-
if os_name ==
|
|
73
|
-
cmd =
|
|
77
|
+
if os_name == "nt": # windows:
|
|
78
|
+
cmd = "ipconfig"
|
|
74
79
|
else:
|
|
75
|
-
cmd =
|
|
80
|
+
cmd = "ip address || ifconfig 2>/dev/null"
|
|
76
81
|
|
|
77
82
|
for s in popen(cmd).readlines():
|
|
78
83
|
addr = ip_pattern.search(s)
|
|
@@ -81,16 +86,16 @@ def _ip_regex_match(parrent_regex, match_regex):
|
|
|
81
86
|
|
|
82
87
|
|
|
83
88
|
def regex_v4(reg): # ipv4 正则提取
|
|
84
|
-
if os_name ==
|
|
85
|
-
regex_str = r
|
|
89
|
+
if os_name == "nt": # Windows: IPv4 xxx: 192.168.1.2
|
|
90
|
+
regex_str = r"IPv4 .*: ((?:\d{1,3}\.){3}\d{1,3})\W"
|
|
86
91
|
else:
|
|
87
|
-
regex_str = r
|
|
92
|
+
regex_str = r"inet (?:addr\:)?((?:\d{1,3}\.){3}\d{1,3})[\s/]"
|
|
88
93
|
return _ip_regex_match(regex_str, reg)
|
|
89
94
|
|
|
90
95
|
|
|
91
96
|
def regex_v6(reg): # ipv6 正则提取
|
|
92
|
-
if os_name ==
|
|
93
|
-
regex_str = r
|
|
97
|
+
if os_name == "nt": # Windows: IPv4 xxx: ::1
|
|
98
|
+
regex_str = r"IPv6 .*: ([\:\dabcdef]*)?\W"
|
|
94
99
|
else:
|
|
95
|
-
regex_str = r
|
|
100
|
+
regex_str = r"inet6 (?:addr\:\s*)?([\:\dabcdef]*)?[\s/%]"
|
|
96
101
|
return _ip_regex_match(regex_str, reg)
|
ddns/provider/__init__.py
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
from ._base import SimpleProvider
|
|
3
|
+
from .alidns import AlidnsProvider
|
|
4
|
+
from .aliesa import AliesaProvider
|
|
5
|
+
from .callback import CallbackProvider
|
|
6
|
+
from .cloudflare import CloudflareProvider
|
|
7
|
+
from .debug import DebugProvider
|
|
8
|
+
from .dnscom import DnscomProvider
|
|
9
|
+
from .dnspod import DnspodProvider
|
|
10
|
+
from .dnspod_com import DnspodComProvider
|
|
11
|
+
from .edgeone import EdgeOneProvider
|
|
12
|
+
from .he import HeProvider
|
|
13
|
+
from .huaweidns import HuaweiDNSProvider
|
|
14
|
+
from .namesilo import NamesiloProvider
|
|
15
|
+
from .noip import NoipProvider
|
|
16
|
+
from .tencentcloud import TencentCloudProvider
|
|
17
|
+
|
|
18
|
+
__all__ = ["SimpleProvider", "get_provider_class"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_provider_class(provider_name):
|
|
22
|
+
# type: (str) -> type[SimpleProvider]
|
|
23
|
+
"""
|
|
24
|
+
获取指定的DNS提供商类
|
|
25
|
+
|
|
26
|
+
:param provider_name: 提供商名称
|
|
27
|
+
:return: 对应的DNS提供商类
|
|
28
|
+
"""
|
|
29
|
+
provider_name = str(provider_name).lower()
|
|
30
|
+
mapping = {
|
|
31
|
+
# dnspod.cn
|
|
32
|
+
"dnspod": DnspodProvider,
|
|
33
|
+
"dnspod_cn": DnspodProvider, # 兼容旧的dnspod_cn
|
|
34
|
+
# dnspod.com
|
|
35
|
+
"dnspod_com": DnspodComProvider,
|
|
36
|
+
"dnspod_global": DnspodComProvider, # 兼容旧的dnspod_global
|
|
37
|
+
# tencent cloud dnspod
|
|
38
|
+
"tencentcloud": TencentCloudProvider,
|
|
39
|
+
"tencent": TencentCloudProvider, # 兼容tencent
|
|
40
|
+
"qcloud": TencentCloudProvider, # 兼容qcloud
|
|
41
|
+
# tencent cloud edgeone
|
|
42
|
+
"edgeone": EdgeOneProvider,
|
|
43
|
+
"teo": EdgeOneProvider, # 兼容teo (EdgeOne产品的API名称)
|
|
44
|
+
"tencentedgeone": EdgeOneProvider, # 兼容tencentedgeone
|
|
45
|
+
# cloudflare
|
|
46
|
+
"cloudflare": CloudflareProvider,
|
|
47
|
+
# aliyun alidns
|
|
48
|
+
"alidns": AlidnsProvider,
|
|
49
|
+
"aliyun": AlidnsProvider, # 兼容aliyun
|
|
50
|
+
# aliyun esa
|
|
51
|
+
"aliesa": AliesaProvider,
|
|
52
|
+
"esa": AliesaProvider, # 兼容esa
|
|
53
|
+
# dns.com
|
|
54
|
+
"dnscom": DnscomProvider,
|
|
55
|
+
"51dns": DnscomProvider, # 兼容51dns
|
|
56
|
+
"dns_com": DnscomProvider, # 兼容dns_com
|
|
57
|
+
# he.net
|
|
58
|
+
"he": HeProvider,
|
|
59
|
+
"he_net": HeProvider, # 兼容he.net
|
|
60
|
+
# huawei
|
|
61
|
+
"huaweidns": HuaweiDNSProvider,
|
|
62
|
+
"huawei": HuaweiDNSProvider, # 兼容huawei
|
|
63
|
+
"huaweicloud": HuaweiDNSProvider,
|
|
64
|
+
# namesilo
|
|
65
|
+
"namesilo": NamesiloProvider,
|
|
66
|
+
"namesilo_com": NamesiloProvider, # 兼容namesilo.com
|
|
67
|
+
# no-ip
|
|
68
|
+
"noip": NoipProvider,
|
|
69
|
+
"no-ip": NoipProvider, # 兼容no-ip
|
|
70
|
+
"noip_com": NoipProvider, # 兼容noip.com
|
|
71
|
+
# callback
|
|
72
|
+
"callback": CallbackProvider,
|
|
73
|
+
"webhook": CallbackProvider, # 兼容
|
|
74
|
+
"http": CallbackProvider, # 兼容
|
|
75
|
+
# debug
|
|
76
|
+
"print": DebugProvider,
|
|
77
|
+
"debug": DebugProvider, # 兼容print
|
|
78
|
+
}
|
|
79
|
+
return mapping.get(provider_name) # type: ignore[return-value]
|