ddns 4.0.0b6__py2.py3-none-any.whl → 4.0.0b8__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/__init__.py +19 -0
- run.py → ddns/__main__.py +40 -50
- {dns → ddns/provider}/dnspod_com.py +1 -1
- ddns/util/config.py +313 -0
- {ddns-4.0.0b6.dist-info → ddns-4.0.0b8.dist-info}/METADATA +51 -32
- ddns-4.0.0b8.dist-info/RECORD +21 -0
- ddns-4.0.0b8.dist-info/entry_points.txt +2 -0
- ddns-4.0.0b8.dist-info/top_level.txt +1 -0
- ddns-4.0.0b6.dist-info/RECORD +0 -20
- ddns-4.0.0b6.dist-info/entry_points.txt +0 -2
- ddns-4.0.0b6.dist-info/top_level.txt +0 -3
- util/config.py +0 -227
- {dns → ddns/provider}/__init__.py +0 -0
- {dns → ddns/provider}/alidns.py +0 -0
- {dns → ddns/provider}/callback.py +0 -0
- {dns → ddns/provider}/cloudflare.py +0 -0
- {dns → ddns/provider}/dnscom.py +0 -0
- {dns → ddns/provider}/dnspod.py +0 -0
- {dns → ddns/provider}/he.py +0 -0
- {dns → ddns/provider}/huaweidns.py +0 -0
- {util → ddns/util}/__init__.py +0 -0
- {util → ddns/util}/cache.py +0 -0
- {util → ddns/util}/ip.py +0 -0
- {ddns-4.0.0b6.dist-info → ddns-4.0.0b8.dist-info}/WHEEL +0 -0
- {ddns-4.0.0b6.dist-info → ddns-4.0.0b8.dist-info}/licenses/LICENSE +0 -0
ddns/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
ddns Package
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__description__ = "automatically update DNS records to my IP [域名自动指向本机IP]"
|
|
7
|
+
|
|
8
|
+
# 编译时,版本会被替换
|
|
9
|
+
__version__ = "4.0.0b8"
|
|
10
|
+
|
|
11
|
+
# 时间也会被替换掉
|
|
12
|
+
build_date = "2025-06-19T10:36:16Z"
|
|
13
|
+
|
|
14
|
+
__doc__ = """
|
|
15
|
+
ddns[{}@{}]
|
|
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)
|
run.py → ddns/__main__.py
RENAMED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
1
|
# -*- coding:utf-8 -*-
|
|
3
2
|
"""
|
|
4
3
|
DDNS
|
|
@@ -10,24 +9,16 @@ from os import path, environ, name as os_name
|
|
|
10
9
|
from io import TextIOWrapper
|
|
11
10
|
from subprocess import check_output
|
|
12
11
|
from tempfile import gettempdir
|
|
13
|
-
from logging import basicConfig, info, warning, error, debug
|
|
12
|
+
from logging import basicConfig, info, warning, error, debug, DEBUG, NOTSET
|
|
14
13
|
|
|
15
14
|
import sys
|
|
16
15
|
|
|
17
|
-
from
|
|
18
|
-
from util
|
|
19
|
-
from util.
|
|
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
20
|
|
|
21
|
-
|
|
22
|
-
__description__ = "automatically update DNS records to my IP [域名自动指向本机IP]"
|
|
23
|
-
__doc__ = """
|
|
24
|
-
ddns[%s]
|
|
25
|
-
(i) homepage or docs [文档主页]: https://ddns.newfuture.cc/
|
|
26
|
-
(?) issues or bugs [问题和帮助]: https://github.com/NewFuture/DDNS/issues
|
|
27
|
-
Copyright (c) New Future (MIT License)
|
|
28
|
-
""" % (__version__)
|
|
29
|
-
|
|
30
|
-
environ["DDNS_VERSION"] = "v4.0.0-beta6"
|
|
21
|
+
environ["DDNS_VERSION"] = __version__
|
|
31
22
|
|
|
32
23
|
|
|
33
24
|
def is_false(value):
|
|
@@ -99,44 +90,62 @@ def update_ip(ip_type, cache, dns, proxy_list):
|
|
|
99
90
|
if not domains:
|
|
100
91
|
return None
|
|
101
92
|
if not isinstance(domains, list):
|
|
102
|
-
domains = domains.strip('; ').replace(
|
|
103
|
-
|
|
104
|
-
index_rule = get_config('index' + ip_type, "default")
|
|
93
|
+
domains = domains.strip('; ').replace(',', ';').replace(' ', ';').split(';')
|
|
94
|
+
|
|
95
|
+
index_rule = get_config('index' + ip_type, "default")
|
|
105
96
|
address = get_ip(ip_type, index_rule)
|
|
106
97
|
if not address:
|
|
107
98
|
error('Fail to get %s address!', ipname)
|
|
108
99
|
return False
|
|
109
|
-
|
|
100
|
+
|
|
101
|
+
if cache and (address == cache.get(ipname)):
|
|
110
102
|
info('%s address not changed, using cache.', ipname)
|
|
111
103
|
return True
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
|
|
105
|
+
record_type = 'A' if ip_type == '4' else 'AAAA'
|
|
106
|
+
update_success = False
|
|
114
107
|
for domain in domains:
|
|
115
|
-
domain = domain.lower()
|
|
108
|
+
domain = domain.lower()
|
|
116
109
|
if change_dns_record(dns, proxy_list, domain=domain, ip=address, record_type=record_type):
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
cache[ipname] =
|
|
110
|
+
update_success = True
|
|
111
|
+
|
|
112
|
+
if isinstance(cache, dict):
|
|
113
|
+
cache[ipname] = update_success and address
|
|
114
|
+
|
|
115
|
+
return update_success
|
|
121
116
|
|
|
122
117
|
|
|
123
118
|
def main():
|
|
124
119
|
"""
|
|
125
120
|
更新
|
|
126
121
|
"""
|
|
127
|
-
|
|
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
128
|
|
|
129
|
+
log_level = get_config('log.level')
|
|
130
|
+
log_format = get_config('log.format', '%(asctime)s %(levelname)s [%(module)s]: %(message)s')
|
|
131
|
+
# Override log format in debug mode to include filename and line number for detailed debugging
|
|
132
|
+
if (log_level == DEBUG or log_level == NOTSET) and not log_format:
|
|
133
|
+
log_format = '%(asctime)s %(levelname)s [%(filename)s:%(lineno)d]: %(message)s'
|
|
129
134
|
basicConfig(
|
|
130
|
-
level=
|
|
131
|
-
format=
|
|
132
|
-
datefmt='%m-%
|
|
135
|
+
level=log_level,
|
|
136
|
+
format=log_format,
|
|
137
|
+
datefmt=get_config('log.datefmt', '%Y-%m-%dT%H:%M:%S'),
|
|
133
138
|
filename=get_config('log.file'),
|
|
134
139
|
)
|
|
140
|
+
|
|
135
141
|
info("DDNS[ %s ] run: %s %s", __version__, os_name, sys.platform)
|
|
136
142
|
|
|
137
143
|
# Dynamically import the dns module as configuration
|
|
138
144
|
dns_provider = str(get_config('dns', 'dnspod').lower())
|
|
139
|
-
|
|
145
|
+
# dns_module = __import__(
|
|
146
|
+
# '.dns', fromlist=[dns_provider], package=__package__)
|
|
147
|
+
dns = getattr(__import__('ddns.provider', fromlist=[dns_provider]), dns_provider)
|
|
148
|
+
# dns = getattr(dns_module, dns_provider)
|
|
140
149
|
dns.Config.ID = get_config('id')
|
|
141
150
|
dns.Config.TOKEN = get_config('token')
|
|
142
151
|
dns.Config.TTL = get_config('ttl')
|
|
@@ -168,23 +177,4 @@ def main():
|
|
|
168
177
|
|
|
169
178
|
|
|
170
179
|
if __name__ == '__main__':
|
|
171
|
-
encode = sys.stdout.encoding
|
|
172
|
-
if encode is not None and encode.lower() != 'utf-8' and hasattr(sys.stdout, 'buffer'):
|
|
173
|
-
# 兼容windows 和部分ASCII编码的老旧系统
|
|
174
|
-
sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
175
|
-
sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
|
176
180
|
main()
|
|
177
|
-
|
|
178
|
-
# Nuitka Project Configuration
|
|
179
|
-
# nuitka-project: --mode=onefile
|
|
180
|
-
# nuitka-project: --output-filename=ddns
|
|
181
|
-
# nuitka-project: --product-name=DDNS
|
|
182
|
-
# nuitka-project: --product-version=0.0.0
|
|
183
|
-
# nuitka-project: --onefile-tempdir-spec="{TEMP}/{PRODUCT}_{VERSION}"
|
|
184
|
-
# nuitka-project: --no-deployment-flag=self-execution
|
|
185
|
-
# nuitka-project: --company-name="New Future"
|
|
186
|
-
# nuitka-project: --copyright=https://ddns.newfuture.cc
|
|
187
|
-
# nuitka-project: --assume-yes-for-downloads
|
|
188
|
-
# nuitka-project: --python-flag=no_site,no_asserts,no_docstrings,isolated,static_hashes
|
|
189
|
-
# nuitka-project: --nofollow-import-to=tkinter,unittest,pydoc,doctest,distutils,setuptools,lib2to3,test,idlelib,lzma
|
|
190
|
-
# nuitka-project: --noinclude-dlls=liblzma.*
|
ddns/util/config.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding:utf-8 -*-
|
|
3
|
+
from argparse import Action, ArgumentParser, Namespace, RawTextHelpFormatter
|
|
4
|
+
from json import load as loadjson, dump as dumpjson
|
|
5
|
+
from os import stat, environ, path
|
|
6
|
+
from logging import error, getLevelName
|
|
7
|
+
from ast import literal_eval
|
|
8
|
+
|
|
9
|
+
import platform
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__cli_args = Namespace()
|
|
14
|
+
__config = {} # type: dict
|
|
15
|
+
log_levels = [
|
|
16
|
+
"CRITICAL",
|
|
17
|
+
"FATAL",
|
|
18
|
+
"ERROR",
|
|
19
|
+
"WARN",
|
|
20
|
+
"WARNING",
|
|
21
|
+
"INFO",
|
|
22
|
+
"DEBUG",
|
|
23
|
+
"NOTSET",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# 支持数组的参数列表
|
|
27
|
+
ARRAY_PARAMS = ["index4", "index6", "ipv4", "ipv6", "proxy"]
|
|
28
|
+
# 简单数组,支持’,’, ‘;’ 分隔的参数列表
|
|
29
|
+
SIMPLE_ARRAY_PARAMS = ["ipv4", "ipv6", "proxy"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def str2bool(v):
|
|
33
|
+
"""
|
|
34
|
+
parse string to boolean
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(v, bool):
|
|
37
|
+
return v
|
|
38
|
+
if v.lower() in ("yes", "true", "t", "y", "1"):
|
|
39
|
+
return True
|
|
40
|
+
elif v.lower() in ("no", "false", "f", "n", "0"):
|
|
41
|
+
return False
|
|
42
|
+
else:
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def log_level(value):
|
|
47
|
+
"""
|
|
48
|
+
parse string to log level
|
|
49
|
+
"""
|
|
50
|
+
return getLevelName(value.upper())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_array_string(value, enable_simple_split):
|
|
54
|
+
"""
|
|
55
|
+
解析数组字符串
|
|
56
|
+
仅当 trim 之后以 '[' 开头以 ']' 结尾时,才尝试使用 ast.literal_eval 解析
|
|
57
|
+
默认返回原始字符串
|
|
58
|
+
"""
|
|
59
|
+
if not isinstance(value, str):
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
trimmed = value.strip()
|
|
63
|
+
if trimmed.startswith("[") and trimmed.endswith("]"):
|
|
64
|
+
try:
|
|
65
|
+
# 尝试使用 ast.literal_eval 解析数组
|
|
66
|
+
parsed_value = literal_eval(trimmed)
|
|
67
|
+
# 确保解析结果是列表或元组
|
|
68
|
+
if isinstance(parsed_value, (list, tuple)):
|
|
69
|
+
return list(parsed_value)
|
|
70
|
+
except (ValueError, SyntaxError) as e:
|
|
71
|
+
# 解析失败时返回原始字符串
|
|
72
|
+
error("Failed to parse array string: %s. Exception: %s", value, e)
|
|
73
|
+
elif enable_simple_split and "," in trimmed:
|
|
74
|
+
# 尝试使用逗号或分号分隔符解析
|
|
75
|
+
return [item.strip() for item in trimmed.split(",") if item.strip()]
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_system_info_str():
|
|
80
|
+
system = platform.system()
|
|
81
|
+
release = platform.release()
|
|
82
|
+
machine = platform.machine()
|
|
83
|
+
arch = platform.architecture()[0] # '64bit' or '32bit'
|
|
84
|
+
|
|
85
|
+
return "{}-{} {} ({})".format(system, release, machine, arch)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_python_info_str():
|
|
89
|
+
version = platform.python_version()
|
|
90
|
+
branch, py_build_date = platform.python_build()
|
|
91
|
+
return "Python-{} {} ({})".format(version, branch, py_build_date)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def init_config(description, doc, version, date):
|
|
95
|
+
"""
|
|
96
|
+
配置
|
|
97
|
+
"""
|
|
98
|
+
global __cli_args
|
|
99
|
+
parser = ArgumentParser(
|
|
100
|
+
description=description, epilog=doc, formatter_class=RawTextHelpFormatter
|
|
101
|
+
)
|
|
102
|
+
sysinfo = get_system_info_str()
|
|
103
|
+
pyinfo = get_python_info_str()
|
|
104
|
+
version_str = "v{} ({})\n{}\n{}".format(version, date, pyinfo, sysinfo)
|
|
105
|
+
parser.add_argument("-v", "--version", action="version", version=version_str)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"-c", "--config", metavar="FILE", help="load config file [配置文件路径]"
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--debug",
|
|
111
|
+
action="store_true",
|
|
112
|
+
help="debug mode [调试模式等效 --log.level=DEBUG]",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# 参数定义
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--dns",
|
|
118
|
+
help="DNS provider [DNS服务提供商]",
|
|
119
|
+
choices=[
|
|
120
|
+
"alidns",
|
|
121
|
+
"cloudflare",
|
|
122
|
+
"dnscom",
|
|
123
|
+
"dnspod",
|
|
124
|
+
"dnspod_com",
|
|
125
|
+
"he",
|
|
126
|
+
"huaweidns",
|
|
127
|
+
"callback",
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument("--id", help="API ID or email [对应账号ID或邮箱]")
|
|
131
|
+
parser.add_argument("--token", help="API token or key [授权凭证或密钥]")
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--index4",
|
|
134
|
+
nargs="*",
|
|
135
|
+
action=ExtendAction,
|
|
136
|
+
metavar="RULE",
|
|
137
|
+
help="IPv4 rules [获取IPv4方式, 多次可配置多规则]",
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--index6",
|
|
141
|
+
nargs="*",
|
|
142
|
+
action=ExtendAction,
|
|
143
|
+
metavar="RULE",
|
|
144
|
+
help="IPv6 rules [获取IPv6方式, 多次可配置多规则]",
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--ipv4",
|
|
148
|
+
nargs="*",
|
|
149
|
+
action=ExtendAction,
|
|
150
|
+
metavar="DOMAIN",
|
|
151
|
+
help="IPv4 domains [IPv4域名列表, 可配置多个域名]",
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--ipv6",
|
|
155
|
+
nargs="*",
|
|
156
|
+
action=ExtendAction,
|
|
157
|
+
metavar="DOMAIN",
|
|
158
|
+
help="IPv6 domains [IPv6域名列表, 可配置多个域名]",
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument("--ttl", type=int, help="DNS TTL(s) [设置域名解析过期时间]")
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--proxy",
|
|
163
|
+
nargs="*",
|
|
164
|
+
action=ExtendAction,
|
|
165
|
+
help="HTTP proxy [设置http代理,可配多个代理连接]",
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--cache",
|
|
169
|
+
type=str2bool,
|
|
170
|
+
nargs="?",
|
|
171
|
+
const=True,
|
|
172
|
+
help="set cache [启用缓存开关,或传入保存路径]",
|
|
173
|
+
)
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--no-cache",
|
|
176
|
+
dest="cache",
|
|
177
|
+
action="store_const",
|
|
178
|
+
const=False,
|
|
179
|
+
help="disable cache [关闭缓存等效 --cache=false]",
|
|
180
|
+
)
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"--log.file", metavar="FILE", help="log file [日志文件,默认标准输出]"
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument("--log.level", type=log_level, metavar="|".join(log_levels))
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--log.format", metavar="FORMAT", help="log format [设置日志打印格式]"
|
|
187
|
+
)
|
|
188
|
+
parser.add_argument(
|
|
189
|
+
"--log.datefmt", metavar="FORMAT", help="date format [日志时间打印格式]"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
__cli_args = parser.parse_args()
|
|
193
|
+
if __cli_args.debug:
|
|
194
|
+
# 如果启用调试模式,则设置日志级别为 DEBUG
|
|
195
|
+
setattr(__cli_args, "log.level", log_level("DEBUG"))
|
|
196
|
+
|
|
197
|
+
is_configfile_required = not get_config("token") and not get_config("id")
|
|
198
|
+
config_file = get_config("config")
|
|
199
|
+
if not config_file:
|
|
200
|
+
# 未指定配置文件且需要读取文件时,依次查找
|
|
201
|
+
cfgs = [
|
|
202
|
+
path.abspath("config.json"),
|
|
203
|
+
path.expanduser("~/.ddns/config.json"),
|
|
204
|
+
"/etc/ddns/config.json",
|
|
205
|
+
]
|
|
206
|
+
config_file = next((cfg for cfg in cfgs if path.isfile(cfg)), cfgs[0])
|
|
207
|
+
|
|
208
|
+
if path.isfile(config_file):
|
|
209
|
+
__load_config(config_file)
|
|
210
|
+
__cli_args.config = config_file
|
|
211
|
+
elif is_configfile_required:
|
|
212
|
+
error("Config file is required, but not found: %s", config_file)
|
|
213
|
+
# 如果需要配置文件但没有指定,则自动生成
|
|
214
|
+
if generate_config(config_file):
|
|
215
|
+
sys.stdout.write("Default configure file %s is generated.\n" % config_file)
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
else:
|
|
218
|
+
sys.exit("fail to load config from file: %s\n" % config_file)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def __load_config(config_path):
|
|
222
|
+
"""
|
|
223
|
+
加载配置
|
|
224
|
+
"""
|
|
225
|
+
global __config
|
|
226
|
+
try:
|
|
227
|
+
with open(config_path, "r") as configfile:
|
|
228
|
+
__config = loadjson(configfile)
|
|
229
|
+
__config["config_modified_time"] = stat(config_path).st_mtime
|
|
230
|
+
if "log" in __config:
|
|
231
|
+
if "level" in __config["log"] and __config["log"]["level"] is not None:
|
|
232
|
+
__config["log.level"] = log_level(__config["log"]["level"])
|
|
233
|
+
if "file" in __config["log"]:
|
|
234
|
+
__config["log.file"] = __config["log"]["file"]
|
|
235
|
+
if "format" in __config["log"]:
|
|
236
|
+
__config["log.format"] = __config["log"]["format"]
|
|
237
|
+
if "datefmt" in __config["log"]:
|
|
238
|
+
__config["log.datefmt"] = __config["log"]["datefmt"]
|
|
239
|
+
elif "log.level" in __config:
|
|
240
|
+
__config["log.level"] = log_level(__config["log.level"])
|
|
241
|
+
except Exception as e:
|
|
242
|
+
error("Failed to load config file `%s`: %s", config_path, e)
|
|
243
|
+
raise
|
|
244
|
+
# 重新抛出异常
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_config(key, default=None):
|
|
248
|
+
"""
|
|
249
|
+
读取配置
|
|
250
|
+
1. 命令行参数
|
|
251
|
+
2. 配置文件
|
|
252
|
+
3. 环境变量
|
|
253
|
+
"""
|
|
254
|
+
if hasattr(__cli_args, key) and getattr(__cli_args, key) is not None:
|
|
255
|
+
return getattr(__cli_args, key)
|
|
256
|
+
if key in __config:
|
|
257
|
+
return __config.get(key)
|
|
258
|
+
# 检查环境变量
|
|
259
|
+
env_name = "DDNS_" + key.replace(".", "_") # type:str
|
|
260
|
+
variations = [env_name, env_name.upper(), env_name.lower()]
|
|
261
|
+
value = next((environ.get(v) for v in variations if v in environ), None)
|
|
262
|
+
|
|
263
|
+
# 如果找到环境变量值且参数支持数组,尝试解析为数组
|
|
264
|
+
if value is not None and key in ARRAY_PARAMS:
|
|
265
|
+
return parse_array_string(value, key in SIMPLE_ARRAY_PARAMS)
|
|
266
|
+
|
|
267
|
+
return value if value is not None else default
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class ExtendAction(Action):
|
|
271
|
+
"""
|
|
272
|
+
兼容 Python <3.8 的 extend action
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
276
|
+
items = getattr(namespace, self.dest, None)
|
|
277
|
+
if items is None:
|
|
278
|
+
items = []
|
|
279
|
+
# values 可能是单个值或列表
|
|
280
|
+
if isinstance(values, list):
|
|
281
|
+
items.extend(values)
|
|
282
|
+
else:
|
|
283
|
+
items.append(values)
|
|
284
|
+
setattr(namespace, self.dest, items)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def generate_config(config_path):
|
|
288
|
+
"""
|
|
289
|
+
生成配置文件
|
|
290
|
+
"""
|
|
291
|
+
configure = {
|
|
292
|
+
"$schema": "https://ddns.newfuture.cc/schema/v4.0.json",
|
|
293
|
+
"id": "YOUR ID or EMAIL for DNS Provider",
|
|
294
|
+
"token": "YOUR TOKEN or KEY for DNS Provider",
|
|
295
|
+
"dns": "dnspod",
|
|
296
|
+
"ipv4": ["newfuture.cc", "ddns.newfuture.cc"],
|
|
297
|
+
"ipv6": ["newfuture.cc", "ipv6.ddns.newfuture.cc"],
|
|
298
|
+
"index4": "default",
|
|
299
|
+
"index6": "default",
|
|
300
|
+
"ttl": None,
|
|
301
|
+
"proxy": None,
|
|
302
|
+
"log": {"level": "INFO"},
|
|
303
|
+
}
|
|
304
|
+
try:
|
|
305
|
+
with open(config_path, "w") as f:
|
|
306
|
+
dumpjson(configure, f, indent=2, sort_keys=True)
|
|
307
|
+
return True
|
|
308
|
+
except IOError:
|
|
309
|
+
error("Cannot open config file to write: `%s`!", config_path)
|
|
310
|
+
return False
|
|
311
|
+
except Exception as e:
|
|
312
|
+
error("Failed to write config file `%s`: %s", config_path, e)
|
|
313
|
+
return False
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddns
|
|
3
|
-
Version: 4.0.
|
|
4
|
-
Summary:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Project-URL:
|
|
3
|
+
Version: 4.0.0b8
|
|
4
|
+
Summary: Dynamic DNS client for multiple providers, supporting IPv4 and IPv6.
|
|
5
|
+
Author-email: NewFuture <python@newfuture.cc>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://ddns.newfuture.cc
|
|
8
|
+
Project-URL: Documentation, https://ddns.newfuture.cc
|
|
9
|
+
Project-URL: Repository, https://github.com/NewFuture/DDNS
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/NewFuture/DDNS/issues
|
|
10
11
|
Project-URL: Source, https://github.com/NewFuture/DDNS
|
|
11
|
-
Keywords: ddns
|
|
12
|
+
Keywords: ddns,ipv6,ipv4,dns,dnspod,alidns,cloudflare
|
|
12
13
|
Platform: any
|
|
13
14
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -19,27 +20,25 @@ Classifier: Topic :: Internet
|
|
|
19
20
|
Classifier: Topic :: Internet :: Name Service (DNS)
|
|
20
21
|
Classifier: Topic :: System :: Networking
|
|
21
22
|
Classifier: Topic :: Software Development
|
|
22
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
23
23
|
Classifier: Programming Language :: Python :: 2.7
|
|
24
24
|
Classifier: Programming Language :: Python :: 3
|
|
25
|
-
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
33
|
+
Requires-Python: >=2.7
|
|
26
34
|
Description-Content-Type: text/markdown
|
|
27
35
|
License-File: LICENSE
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Dynamic: description
|
|
32
|
-
Dynamic: description-content-type
|
|
33
|
-
Dynamic: home-page
|
|
34
|
-
Dynamic: keywords
|
|
35
|
-
Dynamic: license
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: black; extra == "dev"
|
|
38
|
+
Requires-Dist: flake8; extra == "dev"
|
|
36
39
|
Dynamic: license-file
|
|
37
|
-
Dynamic: platform
|
|
38
|
-
Dynamic: project-url
|
|
39
|
-
Dynamic: requires-python
|
|
40
|
-
Dynamic: summary
|
|
41
40
|
|
|
42
|
-
# [<img src="doc/img/ddns.svg" width="32px" height="32px"/>](https://ddns.newfuture.cc) [DDNS](https://github.com/NewFuture/DDNS)
|
|
41
|
+
# [<img src="https://ddns.newfuture.cc/doc/img/ddns.svg" width="32px" height="32px"/>](https://ddns.newfuture.cc) [DDNS](https://github.com/NewFuture/DDNS)
|
|
43
42
|
|
|
44
43
|
> 自动更新 DNS 解析 到本机 IP 地址,支持 IPv4 和 IPv6,本地(内网)IP 和公网 IP。
|
|
45
44
|
> 代理模式,支持自动创建 DNS 记录。
|
|
@@ -60,9 +59,9 @@ Dynamic: summary
|
|
|
60
59
|
- [二进制文件](https://github.com/NewFuture/DDNS/releases/latest) 
|
|
61
60
|
|
|
62
61
|
- 配置方式:
|
|
63
|
-
- [命令行参数](
|
|
64
|
-
- [JSON 配置文件](
|
|
65
|
-
- [环境变量配置](doc/env.
|
|
62
|
+
- [命令行参数](https://ddns.newfuture.cc/doc/cli.html)
|
|
63
|
+
- [JSON 配置文件](https://ddns.newfuture.cc/doc/json.html)
|
|
64
|
+
- [环境变量配置](https://ddns.newfuture.cc/doc/env.html)
|
|
66
65
|
|
|
67
66
|
- 域名支持:
|
|
68
67
|
- 多个域名支持
|
|
@@ -138,6 +137,8 @@ Dynamic: summary
|
|
|
138
137
|
newfuture/ddns
|
|
139
138
|
```
|
|
140
139
|
|
|
140
|
+
更多详细说明和高级用法请查看 [Docker 使用文档](doc/docker.md)。
|
|
141
|
+
|
|
141
142
|
### ② 快速配置
|
|
142
143
|
|
|
143
144
|
1. 申请 api `token`,填写到对应的 `id` 和 `token` 字段:
|
|
@@ -155,12 +156,27 @@ Dynamic: summary
|
|
|
155
156
|
|
|
156
157
|
## 详细配置
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
所有字段可通过三种方式进行配置,优先级为:**命令行参数 > JSON配置文件 > 环境变量**
|
|
159
160
|
|
|
160
|
-
1. 命令行参数 `ddns --key=value`(`ddns -h` 查看详情),优先级最高
|
|
161
|
-
2. JSON
|
|
161
|
+
1. [命令行参数](doc/cli.md) `ddns --key=value`(`ddns -h` 查看详情),优先级最高
|
|
162
|
+
2. [JSON 配置文件](doc/json.md)(值为 null 认为是有效值,会覆盖环境变量的设置,如果没有对应的 key 则会尝试使用环境变量)
|
|
162
163
|
3. 环境变量 DDNS_ 前缀加上 key 全大写或者全小写,点转下划线(`${ddns_id}` 或 `${DDNS_ID}`,`${DDNS_LOG_LEVEL}`)
|
|
163
164
|
|
|
165
|
+
### 配置优先级和字段覆盖关系
|
|
166
|
+
|
|
167
|
+
如果同一个配置项在多个地方设置,将按照以下优先级规则生效:
|
|
168
|
+
|
|
169
|
+
- **命令行参数**:优先级最高,会覆盖其他所有设置
|
|
170
|
+
- **JSON配置文件**:介于命令行和环境变量之间,会覆盖环境变量中的设置
|
|
171
|
+
- **环境变量**:优先级最低,当其他方式未设置时使用
|
|
172
|
+
|
|
173
|
+
**特殊情况**:
|
|
174
|
+
- JSON配置中明确设为`null`的值会覆盖环境变量设置
|
|
175
|
+
- `debug`参数只在命令行中有效,JSON配置文件中的同名设置无效
|
|
176
|
+
- 多值参数(如`ipv4`、`ipv6`等)在命令行中使用方式为重复使用参数,如`--ipv4 domain1 --ipv4 domain2`
|
|
177
|
+
|
|
178
|
+
各配置方式的详细说明请查看对应文档:[命令行](doc/cli.md)、[JSON配置](doc/json.md)、[环境变量](doc/env.md)
|
|
179
|
+
|
|
164
180
|
> 📖 **环境变量详细配置**: 查看 [环境变量配置文档](doc/env.md) 了解所有环境变量的详细用法和示例
|
|
165
181
|
|
|
166
182
|
<details open>
|
|
@@ -169,6 +185,7 @@ Dynamic: summary
|
|
|
169
185
|
- 首次运行会自动生成一个模板配置文件
|
|
170
186
|
- 可以使用 `-c` 使用指定的配置文件(默认读取当前目录的 config.json)
|
|
171
187
|
- 推荐使用 vscode 等支持 JsonSchema 的编辑器编辑配置文件
|
|
188
|
+
- 查看 [JSON配置文件详细文档](doc/json.md) 了解完整的配置选项和示例
|
|
172
189
|
|
|
173
190
|
```bash
|
|
174
191
|
ddns -c path/to/config.json
|
|
@@ -189,9 +206,9 @@ python run.py -c /path/to/config.json
|
|
|
189
206
|
| index6 | string\|int\|array | No | `"default"` | ipv6 获取方式 | 可设置 `网卡`、`内网`、`公网`、`正则` 等方式 |
|
|
190
207
|
| ttl | number | No | `null` | DNS 解析 TTL 时间 | 不设置采用 DNS 默认策略 |
|
|
191
208
|
| proxy | string\|array | No | 无 | http 代理 `;` 分割 | 多代理逐个尝试直到成功,`DIRECT` 为直连 |
|
|
192
|
-
|
|
|
209
|
+
| debug | bool | No | `false` | 是否开启调试 | 等同于设置 log.level=DEBUG,仅命令行参数`--debug`有效 |
|
|
193
210
|
| cache | string\|bool | No | `true` | 是否缓存记录 | 正常情况打开避免频繁更新,默认位置为临时目录下 `ddns.cache`,也可以指定一个具体路径 |
|
|
194
|
-
| log |
|
|
211
|
+
| log | object | No | `null` | 日志配置(可选) | 日志配置对象,支持`level`、`file`、`format`、`datefmt`参数 |
|
|
195
212
|
|
|
196
213
|
#### index4 和 index6 参数说明
|
|
197
214
|
|
|
@@ -237,7 +254,9 @@ python run.py -c /path/to/config.json
|
|
|
237
254
|
"proxy": "127.0.0.1:1080;DIRECT",
|
|
238
255
|
"log": {
|
|
239
256
|
"level": "DEBUG",
|
|
240
|
-
"file": "dns.log"
|
|
257
|
+
"file": "dns.log",
|
|
258
|
+
"format": "%(asctime)s %(levelname)s [%(module)s]: %(message)s",
|
|
259
|
+
"datefmt": "%Y-%m-%dT%H:%M:%S"
|
|
241
260
|
}
|
|
242
261
|
}
|
|
243
262
|
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
ddns/__init__.py,sha256=7htaDQyYxKJKmhu_wVr2juILqhNO9dENXmJPvPKpkyo,503
|
|
2
|
+
ddns/__main__.py,sha256=TIoMSOmdHay9k4m4rr5449y3tk8I60ruub_rBOxjh4M,6148
|
|
3
|
+
ddns/provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
ddns/provider/alidns.py,sha256=fD8mzr7FJ5QxOWLA3ekoOhjeU1IFepOpxybrgZv2c9k,5782
|
|
5
|
+
ddns/provider/callback.py,sha256=V8AG7kxtS0jIkyD9F7xnGJpu1L3E5KUcNDwOnQjcJKs,3208
|
|
6
|
+
ddns/provider/cloudflare.py,sha256=OBq4TJwVHHiPMM6FJo-bz20FOHPvA-RSFbuGilw3tVw,5647
|
|
7
|
+
ddns/provider/dnscom.py,sha256=Nhyl70JGnZY2IV0j5Ebgy_gYQGFb8hXTshaRG_rC0Ww,5297
|
|
8
|
+
ddns/provider/dnspod.py,sha256=52Hstd92-BkyEAicW5DkexfJiTww5KcNZkKs20HUIFE,6211
|
|
9
|
+
ddns/provider/dnspod_com.py,sha256=touJksX1aPDAaP9yWRLVzbbbCtP8jzrDZSMSqtRlc0s,336
|
|
10
|
+
ddns/provider/he.py,sha256=X_WODK6vDTpHdAvPP_vUltaeAlHeG2jMPbzFkfYxWOg,2142
|
|
11
|
+
ddns/provider/huaweidns.py,sha256=0A2jxcJ5zGaMxMMh7C9SsDewhP2zVRLI8FznB9BBBEc,8748
|
|
12
|
+
ddns/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
ddns/util/cache.py,sha256=LVH5jGZFimXmD7XCQCtZULhagybM7gSjMo-73yqttiA,3338
|
|
14
|
+
ddns/util/config.py,sha256=tmOf0uR2jEqWakCgihGP8oCmpT3jS2HP8F6kVM9_neI,9999
|
|
15
|
+
ddns/util/ip.py,sha256=GZzqrl_nq9JuGHTWhPB0fffpr94ITJA6BCVlS5KlWcE,3836
|
|
16
|
+
ddns-4.0.0b8.dist-info/licenses/LICENSE,sha256=MI-ECjp-Vl7WZLiSPY6r5VwrOReNiICVB1QCXiUGt_s,1111
|
|
17
|
+
ddns-4.0.0b8.dist-info/METADATA,sha256=HjETN-YPVmwJ5oMg0wvWxrLzYJgR4a2IXLDAqzFO9sE,16834
|
|
18
|
+
ddns-4.0.0b8.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
19
|
+
ddns-4.0.0b8.dist-info/entry_points.txt,sha256=2-VbA-WZcjebkZrGKvUCuBBRYF4xQNMoLIoGaS234WU,44
|
|
20
|
+
ddns-4.0.0b8.dist-info/top_level.txt,sha256=Se0wn3T8Bc4pj55dGwVrCe8BFwmFCBwQVHF1bTyV0o0,5
|
|
21
|
+
ddns-4.0.0b8.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ddns
|
ddns-4.0.0b6.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
run.py,sha256=-rJYm36hjdAHhOpxQBAWa7-l2IAZsOqwUnaDNecCeyo,6779
|
|
2
|
-
ddns-4.0.0b6.dist-info/licenses/LICENSE,sha256=MI-ECjp-Vl7WZLiSPY6r5VwrOReNiICVB1QCXiUGt_s,1111
|
|
3
|
-
dns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
dns/alidns.py,sha256=fD8mzr7FJ5QxOWLA3ekoOhjeU1IFepOpxybrgZv2c9k,5782
|
|
5
|
-
dns/callback.py,sha256=V8AG7kxtS0jIkyD9F7xnGJpu1L3E5KUcNDwOnQjcJKs,3208
|
|
6
|
-
dns/cloudflare.py,sha256=OBq4TJwVHHiPMM6FJo-bz20FOHPvA-RSFbuGilw3tVw,5647
|
|
7
|
-
dns/dnscom.py,sha256=Nhyl70JGnZY2IV0j5Ebgy_gYQGFb8hXTshaRG_rC0Ww,5297
|
|
8
|
-
dns/dnspod.py,sha256=52Hstd92-BkyEAicW5DkexfJiTww5KcNZkKs20HUIFE,6211
|
|
9
|
-
dns/dnspod_com.py,sha256=9DW7Oz1f2fYSLDDGdwQKMp9OhRJ_CYB8bfzGhf0fIMI,339
|
|
10
|
-
dns/he.py,sha256=X_WODK6vDTpHdAvPP_vUltaeAlHeG2jMPbzFkfYxWOg,2142
|
|
11
|
-
dns/huaweidns.py,sha256=0A2jxcJ5zGaMxMMh7C9SsDewhP2zVRLI8FznB9BBBEc,8748
|
|
12
|
-
util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
util/cache.py,sha256=LVH5jGZFimXmD7XCQCtZULhagybM7gSjMo-73yqttiA,3338
|
|
14
|
-
util/config.py,sha256=hjwkNYjx9tn1augpsBwqA58dATWnKoLS8MZtVdxsav4,8182
|
|
15
|
-
util/ip.py,sha256=GZzqrl_nq9JuGHTWhPB0fffpr94ITJA6BCVlS5KlWcE,3836
|
|
16
|
-
ddns-4.0.0b6.dist-info/METADATA,sha256=8KxSp1hF6UyrmHWutNRvb4J4HLXxWtz_TNTD9lZ-fNw,15284
|
|
17
|
-
ddns-4.0.0b6.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
18
|
-
ddns-4.0.0b6.dist-info/entry_points.txt,sha256=vpxxBE6Kepc-SHatr-FRRLmZecrTupYejyDB8ovJbLg,34
|
|
19
|
-
ddns-4.0.0b6.dist-info/top_level.txt,sha256=dS3_ClQTIG1ousHykZ1P3AzQa-MKq4jT_rbm21D-KxE,13
|
|
20
|
-
ddns-4.0.0b6.dist-info/RECORD,,
|
util/config.py
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# -*- coding:utf-8 -*-
|
|
3
|
-
from argparse import Action, ArgumentParser, Namespace, RawTextHelpFormatter
|
|
4
|
-
from json import load as loadjson, dump as dumpjson
|
|
5
|
-
from os import stat, environ, path
|
|
6
|
-
from logging import error, getLevelName
|
|
7
|
-
from ast import literal_eval
|
|
8
|
-
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
__cli_args = Namespace()
|
|
13
|
-
__config = {} # type: dict
|
|
14
|
-
log_levels = ['CRITICAL', 'FATAL', 'ERROR',
|
|
15
|
-
'WARN', 'WARNING', 'INFO', 'DEBUG', 'NOTSET']
|
|
16
|
-
|
|
17
|
-
# 支持数组的参数列表
|
|
18
|
-
ARRAY_PARAMS = ['index4', 'index6', 'ipv4', 'ipv6', 'proxy']
|
|
19
|
-
# 简单数组,支持’,’, ‘;’ 分隔的参数列表
|
|
20
|
-
SIMPLE_ARRAY_PARAMS = ['ipv4', 'ipv6', 'proxy']
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def str2bool(v):
|
|
24
|
-
"""
|
|
25
|
-
parse string to boolean
|
|
26
|
-
"""
|
|
27
|
-
if isinstance(v, bool):
|
|
28
|
-
return v
|
|
29
|
-
if v.lower() in ('yes', 'true', 't', 'y', '1'):
|
|
30
|
-
return True
|
|
31
|
-
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
|
|
32
|
-
return False
|
|
33
|
-
else:
|
|
34
|
-
return v
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def log_level(value):
|
|
38
|
-
"""
|
|
39
|
-
parse string to log level
|
|
40
|
-
"""
|
|
41
|
-
return getLevelName(value.upper())
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def parse_array_string(value, enable_simple_split):
|
|
45
|
-
"""
|
|
46
|
-
解析数组字符串
|
|
47
|
-
仅当 trim 之后以 '[' 开头以 ']' 结尾时,才尝试使用 ast.literal_eval 解析
|
|
48
|
-
默认返回原始字符串
|
|
49
|
-
"""
|
|
50
|
-
if not isinstance(value, str):
|
|
51
|
-
return value
|
|
52
|
-
|
|
53
|
-
trimmed = value.strip()
|
|
54
|
-
if trimmed.startswith('[') and trimmed.endswith(']'):
|
|
55
|
-
try:
|
|
56
|
-
# 尝试使用 ast.literal_eval 解析数组
|
|
57
|
-
parsed_value = literal_eval(trimmed)
|
|
58
|
-
# 确保解析结果是列表或元组
|
|
59
|
-
if isinstance(parsed_value, (list, tuple)):
|
|
60
|
-
return list(parsed_value)
|
|
61
|
-
except (ValueError, SyntaxError) as e:
|
|
62
|
-
# 解析失败时返回原始字符串
|
|
63
|
-
error('Failed to parse array string: %s. Exception: %s', value, e)
|
|
64
|
-
elif enable_simple_split and ',' in trimmed:
|
|
65
|
-
# 尝试使用逗号或分号分隔符解析
|
|
66
|
-
return [item.strip() for item in trimmed.split(',') if item.strip()]
|
|
67
|
-
return value
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def init_config(description, doc, version):
|
|
71
|
-
"""
|
|
72
|
-
配置
|
|
73
|
-
"""
|
|
74
|
-
global __cli_args
|
|
75
|
-
parser = ArgumentParser(description=description,
|
|
76
|
-
epilog=doc, formatter_class=RawTextHelpFormatter)
|
|
77
|
-
parser.add_argument('-v', '--version',
|
|
78
|
-
action='version', version=version)
|
|
79
|
-
parser.add_argument('-c', '--config', help='run with config file [配置文件路径]')
|
|
80
|
-
|
|
81
|
-
# 参数定义
|
|
82
|
-
parser.add_argument('--dns', help='DNS Provider [DNS服务提供商]', choices=[
|
|
83
|
-
'alidns', 'cloudflare', 'dnscom', 'dnspod', 'dnspod_com', 'he', 'huaweidns', 'callback'])
|
|
84
|
-
parser.add_argument('--id', help='api ID [授权账户]')
|
|
85
|
-
parser.add_argument('--token', help='api token or Secret key [授权访问凭证或密钥]')
|
|
86
|
-
parser.add_argument('--index4', nargs='*', action=ExtendAction,
|
|
87
|
-
help='list to get ipv4 [IPV4 获取方式]')
|
|
88
|
-
parser.add_argument('--index6', nargs='*', action=ExtendAction,
|
|
89
|
-
help='list to get ipv6 [IPV6获取方式]')
|
|
90
|
-
parser.add_argument('--ipv4', nargs='*', action=ExtendAction,
|
|
91
|
-
help='ipv4 domain list [IPV4域名列表]')
|
|
92
|
-
parser.add_argument('--ipv6', nargs='*', action=ExtendAction,
|
|
93
|
-
help='ipv6 domain list [IPV6域名列表]')
|
|
94
|
-
parser.add_argument('--ttl', type=int, help='ttl for DNS [DNS 解析 TTL 时间]')
|
|
95
|
-
parser.add_argument('--proxy', nargs='*', action=ExtendAction,
|
|
96
|
-
help='https proxy [设置http 代理,多代理逐个尝试直到成功]')
|
|
97
|
-
parser.add_argument('--cache', type=str2bool, nargs='?',
|
|
98
|
-
const=True, help='cache flag [启用缓存,可配配置路径或开关]')
|
|
99
|
-
parser.add_argument('--log.file', metavar='LOG_FILE',
|
|
100
|
-
help='log file [日志文件,默认标准输出]')
|
|
101
|
-
parser.add_argument('--log.level', type=log_level,
|
|
102
|
-
metavar='|'.join(log_levels))
|
|
103
|
-
|
|
104
|
-
__cli_args = parser.parse_args()
|
|
105
|
-
is_configfile_required = not get_config("token") and not get_config("id")
|
|
106
|
-
config_file = get_config("config")
|
|
107
|
-
if not config_file:
|
|
108
|
-
# 未指定配置文件且需要读取文件时,依次查找
|
|
109
|
-
cfgs = [
|
|
110
|
-
path.abspath('config.json'),
|
|
111
|
-
path.expanduser('~/.ddns/config.json'),
|
|
112
|
-
'/etc/ddns/config.json'
|
|
113
|
-
]
|
|
114
|
-
config_file = next((cfg for cfg in cfgs if path.isfile(cfg)), cfgs[0])
|
|
115
|
-
|
|
116
|
-
if path.isfile(config_file):
|
|
117
|
-
__load_config(config_file)
|
|
118
|
-
__cli_args.config = config_file
|
|
119
|
-
elif is_configfile_required:
|
|
120
|
-
error('Config file is required, but not found: %s', config_file)
|
|
121
|
-
# 如果需要配置文件但没有指定,则自动生成
|
|
122
|
-
if generate_config(config_file):
|
|
123
|
-
sys.stdout.write(
|
|
124
|
-
'Default configure file %s is generated.\n' % config_file)
|
|
125
|
-
sys.exit(1)
|
|
126
|
-
else:
|
|
127
|
-
sys.exit('fail to load config from file: %s\n' % config_file)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def __load_config(config_path):
|
|
131
|
-
"""
|
|
132
|
-
加载配置
|
|
133
|
-
"""
|
|
134
|
-
global __config
|
|
135
|
-
try:
|
|
136
|
-
with open(config_path, 'r') as configfile:
|
|
137
|
-
__config = loadjson(configfile)
|
|
138
|
-
__config["config_modified_time"] = stat(config_path).st_mtime
|
|
139
|
-
if 'log' in __config:
|
|
140
|
-
if 'level' in __config['log'] and __config['log']['level'] is not None:
|
|
141
|
-
__config['log.level'] = log_level(__config['log']['level'])
|
|
142
|
-
if 'file' in __config['log']:
|
|
143
|
-
__config['log.file'] = __config['log']['file']
|
|
144
|
-
elif 'log.level' in __config:
|
|
145
|
-
__config['log.level'] = log_level(__config['log.level'])
|
|
146
|
-
except Exception as e:
|
|
147
|
-
error('Failed to load config file `%s`: %s', config_path, e)
|
|
148
|
-
raise
|
|
149
|
-
# 重新抛出异常
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def get_config(key, default=None):
|
|
153
|
-
"""
|
|
154
|
-
读取配置
|
|
155
|
-
1. 命令行参数
|
|
156
|
-
2. 配置文件
|
|
157
|
-
3. 环境变量
|
|
158
|
-
"""
|
|
159
|
-
if hasattr(__cli_args, key) and getattr(__cli_args, key) is not None:
|
|
160
|
-
return getattr(__cli_args, key)
|
|
161
|
-
if key in __config:
|
|
162
|
-
return __config.get(key)
|
|
163
|
-
# 检查环境变量
|
|
164
|
-
env_name = 'DDNS_' + key.replace('.', '_') # type:str
|
|
165
|
-
variations = [env_name, env_name.upper(), env_name.lower()]
|
|
166
|
-
value = next((environ.get(v) for v in variations if v in environ), None)
|
|
167
|
-
|
|
168
|
-
# 如果找到环境变量值且参数支持数组,尝试解析为数组
|
|
169
|
-
if value is not None and key in ARRAY_PARAMS:
|
|
170
|
-
return parse_array_string(value, key in SIMPLE_ARRAY_PARAMS)
|
|
171
|
-
|
|
172
|
-
return value if value is not None else default
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
class ExtendAction(Action):
|
|
176
|
-
"""
|
|
177
|
-
兼容 Python <3.8 的 extend action
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
def __call__(self, parser, namespace, values, option_string=None):
|
|
181
|
-
items = getattr(namespace, self.dest, None)
|
|
182
|
-
if items is None:
|
|
183
|
-
items = []
|
|
184
|
-
# values 可能是单个值或列表
|
|
185
|
-
if isinstance(values, list):
|
|
186
|
-
items.extend(values)
|
|
187
|
-
else:
|
|
188
|
-
items.append(values)
|
|
189
|
-
setattr(namespace, self.dest, items)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def generate_config(config_path):
|
|
193
|
-
"""
|
|
194
|
-
生成配置文件
|
|
195
|
-
"""
|
|
196
|
-
configure = {
|
|
197
|
-
'$schema': 'https://ddns.newfuture.cc/schema/v4.0.json',
|
|
198
|
-
'id': 'YOUR ID or EMAIL for DNS Provider',
|
|
199
|
-
'token': 'YOUR TOKEN or KEY for DNS Provider',
|
|
200
|
-
'dns': 'dnspod',
|
|
201
|
-
'ipv4': [
|
|
202
|
-
'newfuture.cc',
|
|
203
|
-
'ddns.newfuture.cc'
|
|
204
|
-
],
|
|
205
|
-
'ipv6': [
|
|
206
|
-
'newfuture.cc',
|
|
207
|
-
'ipv6.ddns.newfuture.cc'
|
|
208
|
-
],
|
|
209
|
-
'index4': 'default',
|
|
210
|
-
'index6': 'default',
|
|
211
|
-
'ttl': None,
|
|
212
|
-
'proxy': None,
|
|
213
|
-
'log': {
|
|
214
|
-
'level': 'INFO',
|
|
215
|
-
'file': None
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
try:
|
|
219
|
-
with open(config_path, 'w') as f:
|
|
220
|
-
dumpjson(configure, f, indent=2, sort_keys=True)
|
|
221
|
-
return True
|
|
222
|
-
except IOError:
|
|
223
|
-
error('Cannot open config file to write: `%s`!', config_path)
|
|
224
|
-
return False
|
|
225
|
-
except Exception as e:
|
|
226
|
-
error('Failed to write config file `%s`: %s', config_path, e)
|
|
227
|
-
return False
|
|
File without changes
|
{dns → ddns/provider}/alidns.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dns → ddns/provider}/dnscom.py
RENAMED
|
File without changes
|
{dns → ddns/provider}/dnspod.py
RENAMED
|
File without changes
|
{dns → ddns/provider}/he.py
RENAMED
|
File without changes
|
|
File without changes
|
{util → ddns/util}/__init__.py
RENAMED
|
File without changes
|
{util → ddns/util}/cache.py
RENAMED
|
File without changes
|
{util → ddns/util}/ip.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|