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 ADDED
@@ -0,0 +1,5 @@
1
+ # coding=utf-8
2
+ # flake8: noqa: F401
3
+ from typing import *
4
+ from .provider import SimpleProvider
5
+ import logging
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.2"
9
+ __version__ = "4.1.0b2"
10
10
 
11
11
  # 时间也会被替换掉
12
- build_date = "2025-06-25T01:07:58Z"
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: New Future
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 tempfile import gettempdir
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__, __doc__, build_date
17
- from .util import ip
18
- from .util.cache import Cache
19
- from .util.config import init_config, get_config
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 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, 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(
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
- return dns.update_record(domain, kw['ip'], record_type=record_type)
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 update %s record for %s: %s", record_type, domain, e)
81
- return False
45
+ logger.error("Failed to get %s address: %s", ip_type, e)
46
+ return None
82
47
 
83
48
 
84
- def update_ip(ip_type, cache, dns, proxy_list):
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")
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
- 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
- if change_dns_record(dns, proxy_list, domain=domain, ip=address, record_type=record_type):
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
- if isinstance(cache, dict):
113
- cache[ipname] = update_success and address
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 main():
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() != 'utf-8' and hasattr(sys.stdout, 'buffer'):
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='utf-8')
126
- sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
127
- init_config(__description__, __doc__, __version__, build_date)
128
-
129
- log_level = get_config('log.level', INFO)
130
- log_format = get_config('log.format')
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
- 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
-
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
@@ -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
- try: # python3
8
- from urllib.request import urlopen, Request
9
- except ImportError: # python2
10
- from urllib2 import urlopen, Request
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'((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
+ 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'((([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
+ 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(('1:1:1:1:1:1:1:1', 8))
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
- res = urlopen(
51
- Request(url, headers={'User-Agent': 'Mozilla/5.0 ddns'}), timeout=60
52
- ).read().decode('utf8', 'ignore')
53
- debug("response: %s", res)
54
- return compile(reg).search(res).group()
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 == 'nt': # windows:
73
- cmd = 'ipconfig'
77
+ if os_name == "nt": # windows:
78
+ cmd = "ipconfig"
74
79
  else:
75
- cmd = 'ip address || ifconfig 2>/dev/null'
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 == 'nt': # Windows: IPv4 xxx: 192.168.1.2
85
- regex_str = r'IPv4 .*: ((?:\d{1,3}\.){3}\d{1,3})\W'
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'inet (?:addr\:)?((?:\d{1,3}\.){3}\d{1,3})[\s/]'
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 == 'nt': # Windows: IPv4 xxx: ::1
93
- regex_str = r'IPv6 .*: ([\:\dabcdef]*)?\W'
97
+ if os_name == "nt": # Windows: IPv4 xxx: ::1
98
+ regex_str = r"IPv6 .*: ([\:\dabcdef]*)?\W"
94
99
  else:
95
- regex_str = r'inet6 (?:addr\:\s*)?([\:\dabcdef]*)?[\s/%]'
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]