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 CHANGED
@@ -6,16 +6,7 @@ ddns Package
6
6
  __description__ = "automatically update DNS records to my IP [域名自动指向本机IP]"
7
7
 
8
8
  # 编译时,版本会被替换
9
- __version__ = "4.1.0b1"
9
+ __version__ = "4.1.0b2"
10
10
 
11
11
  # 时间也会被替换掉
12
- build_date = "2025-07-03T13:20:03Z"
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) NewFuture (MIT License)
19
- """.format(
20
- __version__, build_date
21
- )
12
+ build_date = "2025-07-15T02:27:02Z"
ddns/__main__.py CHANGED
@@ -4,189 +4,114 @@ DDNS
4
4
  @author: NewFuture, rufengsuixing
5
5
  """
6
6
 
7
- from os import path, environ, name as os_name
8
7
  from io import TextIOWrapper
9
8
  from subprocess import check_output
10
- from tempfile import gettempdir
11
- from logging import basicConfig, getLogger, info, error, debug, warning, INFO
12
-
9
+ from logging import getLogger
13
10
  import sys
14
11
 
15
- from .__init__ import __version__, __description__, __doc__, build_date
16
- from .util import ip
17
- from .util.cache import Cache
18
- from .util.config import init_config, get_config
19
- from .provider import get_provider_class, SimpleProvider # noqa: F401
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
- def is_false(value):
25
- """
26
- 判断值是否为 False
27
- 字符串 'false', 或者 False, 或者 'none';
28
- 0 不是 False
29
- """
30
- if hasattr(value, "strip"): # 字符串
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, index="default"):
23
+ def get_ip(ip_type, rules):
36
24
  """
37
25
  get IP address
38
26
  """
39
- # CN: 捕获异常
40
- # EN: Catch exceptions
41
- value = None
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(index[6:], shell=True).strip().decode("utf-8"))
57
- elif index.startswith("url:"): # 自定义 url
58
- value = getattr(ip, "public_v" + ip_type)(index[4:])
59
- elif index.startswith("regex:"): # 正则 regex
60
- value = getattr(ip, "regex_v" + ip_type)(index[6:])
61
- else:
62
- value = getattr(ip, index + "_v" + ip_type)()
63
- except Exception as e:
64
- error("Failed to get %s address: %s", ip_type, e)
65
- return value
66
-
67
-
68
- def change_dns_record(dns, proxy_list, **kw):
69
- # type: (SimpleProvider, list, **(str)) -> bool
70
- for proxy in proxy_list:
71
- if not proxy or (proxy.upper() in ["DIRECT", "NONE"]):
72
- dns.set_proxy(None)
73
- else:
74
- dns.set_proxy(proxy)
75
- record_type, domain = kw["record_type"], kw["domain"]
27
+ if rules is False: # disabled
28
+ return False
29
+ for i in rules:
76
30
  try:
77
- return dns.set_record(domain, kw["ip"], record_type=record_type, ttl=kw["ttl"], line=kw.get("line"))
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)()
78
44
  except Exception as e:
79
- error("Failed to update %s record for %s: %s", record_type, domain, e)
80
- return False
45
+ logger.error("Failed to get %s address: %s", ip_type, e)
46
+ return None
81
47
 
82
48
 
83
- def update_ip(ip_type, cache, dns, ttl, line, proxy_list):
84
- # type: (str, Cache | None, SimpleProvider, str, str | None, list[str]) -> bool | None
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
- index_rule = get_config("index" + ip_type, "default") # type: str # type: ignore
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("Fail to get %s address!", ipname)
60
+ logger.error("Fail to get %s address!", ip_type)
99
61
  return False
100
62
 
101
- record_type = "A" if ip_type == "4" else "AAAA"
102
63
  update_success = False
103
64
 
104
- # Check cache and update each domain individually
105
65
  for domain in domains:
106
66
  domain = domain.lower()
107
67
  cache_key = "{}:{}".format(domain, record_type)
108
68
  if cache and cache.get(cache_key) == address:
109
- info("%s[%s] address not changed, using cache: %s", domain, record_type, address)
110
- update_success = True # At least one domain is successfully cached
69
+ logger.info("%s[%s] address not changed, using cache: %s", domain, record_type, address)
70
+ update_success = True
111
71
  else:
112
- # Update domain that is not cached or has different IP
113
- if change_dns_record(dns, proxy_list, domain=domain, ip=address, record_type=record_type, ttl=ttl, line=line):
114
- warning("set %s[IPv%s]: %s successfully.", domain, ip_type, address)
115
- update_success = True
116
- # Cache successful update immediately
117
- if isinstance(cache, dict):
118
- cache[cache_key] = address
119
-
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)
120
83
  return update_success
121
84
 
122
85
 
123
- def main():
86
+ def run(config):
87
+ # type: (Config) -> bool
124
88
  """
125
- 更新
89
+ Run the DDNS update process
126
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():
127
107
  encode = sys.stdout.encoding
128
108
  if encode is not None and encode.lower() != "utf-8" and hasattr(sys.stdout, "buffer"):
129
109
  # 兼容windows 和部分ASCII编码的老旧系统
130
110
  sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
131
111
  sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
132
- init_config(__description__, __doc__, __version__, build_date)
133
-
134
- log_level = get_config("log.level", INFO) # type: int # type: ignore
135
- log_format = get_config("log.format") # type: str | None # type: ignore
136
- if log_format:
137
- # A custom log format is already set; no further action is required.
138
- pass
139
- elif log_level < INFO:
140
- # Override log format in debug mode to include filename and line number for detailed debugging
141
- log_format = "%(asctime)s %(levelname)s [%(name)s.%(funcName)s](%(filename)s:%(lineno)d): %(message)s"
142
- elif log_level > INFO:
143
- log_format = "%(asctime)s %(levelname)s: %(message)s"
144
- else:
145
- log_format = "%(asctime)s %(levelname)s [%(name)s]: %(message)s"
146
- basicConfig(
147
- level=log_level,
148
- format=log_format,
149
- datefmt=get_config("log.datefmt", "%Y-%m-%dT%H:%M:%S"), # type: ignore
150
- filename=get_config("log.file"), # type: ignore
151
- )
152
- logger = getLogger()
153
112
  logger.name = "ddns"
154
-
155
- debug("DDNS[ %s ] run: %s %s", __version__, os_name, sys.platform)
156
-
157
- # dns provider class
158
- dns_name = get_config("dns", "debug") # type: str # type: ignore
159
- provider_class = get_provider_class(dns_name)
160
- ssl_config = get_config("ssl", "auto") # type: str | bool # type: ignore
161
- dns = provider_class(get_config("id"), get_config("token"), logger=logger, verify_ssl=ssl_config) # type: ignore
162
-
163
- if get_config("config"):
164
- info("loaded Config from: %s", path.abspath(get_config("config"))) # type: ignore
165
-
166
- proxy = get_config("proxy") or "DIRECT"
167
- proxy_list = proxy if isinstance(proxy, list) else proxy.strip(";").replace(",", ";").split(";")
168
-
169
- cache_config = get_config("cache", True) # type: bool | str # type: ignore
170
- if cache_config is False:
171
- cache = None
172
- elif cache_config is True:
173
- cache = Cache(path.join(gettempdir(), "ddns.cache"), logger)
174
- else:
175
- cache = Cache(cache_config, logger)
176
-
177
- if cache is None:
178
- info("Cache is disabled!")
179
- elif get_config("config_modified_time", float("inf")) >= cache.time: # type: ignore
180
- info("Cache file is outdated.")
181
- cache.clear()
182
- elif len(cache) == 0:
183
- debug("Cache is empty.")
184
- else:
185
- debug("Cache loaded with %d entries.", len(cache))
186
- ttl = get_config("ttl") # type: str # type: ignore
187
- line = get_config("line") # type: str | None # type: ignore
188
- update_ip("4", cache, dns, ttl, line, proxy_list)
189
- update_ip("6", cache, dns, ttl, line, proxy_list)
113
+ config = load_config(__description__, __version__, build_date)
114
+ run(config)
190
115
 
191
116
 
192
117
  if __name__ == "__main__":
@@ -4,10 +4,11 @@ cache module
4
4
  文件缓存
5
5
  """
6
6
 
7
+ from logging import getLogger, Logger # noqa: F401
7
8
  from os import path, stat
8
- from pickle import dump, load
9
+ from json import load, dump
10
+ from tempfile import gettempdir
9
11
  from time import time
10
- from logging import getLogger, Logger # noqa: F401
11
12
 
12
13
 
13
14
  class Cache(dict):
@@ -40,18 +41,20 @@ class Cache(dict):
40
41
  file = self.__filename
41
42
 
42
43
  self.__logger.debug("load cache data from %s", file)
43
- if file and path.isfile(file):
44
- with open(file, "rb") as data:
45
- try:
44
+ if file:
45
+ try:
46
+ with open(file, "r") as data:
46
47
  loaded_data = load(data)
47
48
  self.clear()
48
49
  self.update(loaded_data)
49
50
  self.__time = stat(file).st_mtime
50
51
  return self
51
- except ValueError:
52
- pass
53
- except Exception as e:
54
- self.__logger.warning(e)
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)
55
58
  else:
56
59
  self.__logger.info("cache file not exist")
57
60
 
@@ -63,10 +66,10 @@ class Cache(dict):
63
66
  def sync(self):
64
67
  """Sync the write buffer with the cache files and clear the buffer."""
65
68
  if self.__changed and self.__filename:
66
- with open(self.__filename, "wb") as data:
69
+ with open(self.__filename, "w") as data:
67
70
  # 只保存非私有字段(不以__开头的字段)
68
71
  filtered_data = {k: v for k, v in super(Cache, self).items() if not k.startswith("__")}
69
- dump(filtered_data, data)
72
+ dump(filtered_data, data, separators=(",", ":"))
70
73
  self.__logger.debug("save cache data to %s", self.__filename)
71
74
  self.__time = time()
72
75
  self.__changed = False
@@ -150,3 +153,31 @@ class Cache(dict):
150
153
 
151
154
  def __del__(self):
152
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
@@ -5,10 +5,10 @@ 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
- try: # python3
9
- from urllib.request import urlopen, Request
10
- except ImportError: # python2
11
- from urllib2 import urlopen, Request # type: ignore[import-untyped] # noqa: F401
8
+ from .util.http import send_http_request
9
+
10
+ # 模块级别的SSL验证配置,默认使用auto模式
11
+ ssl_verify = "auto"
12
12
 
13
13
  # IPV4正则
14
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])"
@@ -48,13 +48,15 @@ def local_v4(i=0): # 本地ipv4地址
48
48
  def _open(url, reg):
49
49
  try:
50
50
  debug("open: %s", url)
51
- res = (
52
- urlopen(Request(url, headers={"User-Agent": "Mozilla/5.0 ddns"}), timeout=60)
53
- .read()
54
- .decode("utf8", "ignore")
51
+ response = send_http_request(
52
+ method="GET", url=url, headers={"User-Agent": "Mozilla/5.0 ddns"}, verify_ssl=ssl_verify
55
53
  )
54
+ res = response.body
56
55
  debug("response: %s", res)
57
- return compile(reg).search(res).group()
56
+ match = compile(reg).search(res)
57
+ if match:
58
+ return match.group()
59
+ error("No match found in response: %s", res)
58
60
  except Exception as e:
59
61
  error(e)
60
62
 
ddns/provider/__init__.py CHANGED
@@ -1,15 +1,21 @@
1
1
  # coding=utf-8
2
- from ._base import SimpleProvider # noqa: F401
2
+ from ._base import SimpleProvider
3
3
  from .alidns import AlidnsProvider
4
+ from .aliesa import AliesaProvider
4
5
  from .callback import CallbackProvider
5
6
  from .cloudflare import CloudflareProvider
7
+ from .debug import DebugProvider
6
8
  from .dnscom import DnscomProvider
7
9
  from .dnspod import DnspodProvider
8
10
  from .dnspod_com import DnspodComProvider
11
+ from .edgeone import EdgeOneProvider
9
12
  from .he import HeProvider
10
13
  from .huaweidns import HuaweiDNSProvider
14
+ from .namesilo import NamesiloProvider
15
+ from .noip import NoipProvider
11
16
  from .tencentcloud import TencentCloudProvider
12
- from .debug import DebugProvider
17
+
18
+ __all__ = ["SimpleProvider", "get_provider_class"]
13
19
 
14
20
 
15
21
  def get_provider_class(provider_name):
@@ -32,15 +38,22 @@ def get_provider_class(provider_name):
32
38
  "tencentcloud": TencentCloudProvider,
33
39
  "tencent": TencentCloudProvider, # 兼容tencent
34
40
  "qcloud": TencentCloudProvider, # 兼容qcloud
41
+ # tencent cloud edgeone
42
+ "edgeone": EdgeOneProvider,
43
+ "teo": EdgeOneProvider, # 兼容teo (EdgeOne产品的API名称)
44
+ "tencentedgeone": EdgeOneProvider, # 兼容tencentedgeone
35
45
  # cloudflare
36
46
  "cloudflare": CloudflareProvider,
37
47
  # aliyun alidns
38
48
  "alidns": AlidnsProvider,
39
49
  "aliyun": AlidnsProvider, # 兼容aliyun
50
+ # aliyun esa
51
+ "aliesa": AliesaProvider,
52
+ "esa": AliesaProvider, # 兼容esa
40
53
  # dns.com
41
54
  "dnscom": DnscomProvider,
42
- "51dns": DnscomProvider, # 兼容旧的51dns
43
- "dns_com": DnscomProvider, # 兼容旧的dns_com
55
+ "51dns": DnscomProvider, # 兼容51dns
56
+ "dns_com": DnscomProvider, # 兼容dns_com
44
57
  # he.net
45
58
  "he": HeProvider,
46
59
  "he_net": HeProvider, # 兼容he.net
@@ -48,6 +61,13 @@ def get_provider_class(provider_name):
48
61
  "huaweidns": HuaweiDNSProvider,
49
62
  "huawei": HuaweiDNSProvider, # 兼容huawei
50
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
51
71
  # callback
52
72
  "callback": CallbackProvider,
53
73
  "webhook": CallbackProvider, # 兼容