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 ADDED
@@ -0,0 +1,6 @@
1
+ # coding=utf-8
2
+ # flake8: noqa: F401
3
+ # ruff: noqa: F403
4
+ from typing import *
5
+ from .provider import SimpleProvider
6
+ 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.1"
9
+ __version__ = "4.1.0"
10
10
 
11
11
  # 时间也会被替换掉
12
- build_date = "2025-06-23T16:56:46Z"
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: New Future
5
- @modified: rufengsuixing
4
+ @author: NewFuture, rufengsuixing
6
5
  """
7
6
 
8
- from os import path, environ, name as os_name
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
- environ["DDNS_VERSION"] = __version__
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
- # 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)
25
+ if rules is False: # disabled
26
+ return False
27
+ for i in rules:
77
28
  try:
78
- return dns.update_record(domain, kw['ip'], record_type=record_type)
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 update %s record for %s: %s", record_type, domain, e)
81
- return False
43
+ logger.error("Failed to get %s address: %s", ip_type, e)
44
+ return None
82
45
 
83
46
 
84
- def update_ip(ip_type, cache, dns, proxy_list):
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
- index_rule = get_config('index' + ip_type, "default")
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('Fail to get %s address!', ipname)
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
- if change_dns_record(dns, proxy_list, domain=domain, ip=address, record_type=record_type):
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
- if isinstance(cache, dict):
113
- cache[ipname] = update_success and address
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 main():
84
+ def run(config):
85
+ # type: (Config) -> bool
119
86
  """
120
- 更新
87
+ Run the DDNS update process
121
88
  """
122
- encode = sys.stdout.encoding
123
- if encode is not None and encode.lower() != 'utf-8' and hasattr(sys.stdout, 'buffer'):
124
- # 兼容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')
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'),
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
- 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()
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
- debug('Cache is empty.')
182
- update_ip('4', cache, dns, proxy_list)
183
- update_ip('6', cache, dns, proxy_list)
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__ == '__main__':
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)