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/provider/noip.py ADDED
@@ -0,0 +1,103 @@
1
+ # coding=utf-8
2
+ """
3
+ No-IP (noip.com) Dynamic DNS API
4
+ @author: GitHub Copilot
5
+ """
6
+
7
+ import base64
8
+ from ._base import SimpleProvider, TYPE_FORM
9
+
10
+
11
+ class NoipProvider(SimpleProvider):
12
+ """
13
+ No-IP (www.noip.com) Dynamic DNS Provider
14
+
15
+ No-IP is a popular dynamic DNS service that provides simple HTTP-based
16
+ API for updating DNS records. This provider supports the standard
17
+ No-IP update protocol.
18
+ """
19
+
20
+ endpoint = "https://dynupdate.no-ip.com"
21
+ content_type = TYPE_FORM
22
+ accept = None # No-IP returns plain text response
23
+ decode_response = False # Response is plain text, not JSON
24
+
25
+ def _validate(self):
26
+ """
27
+ Validate authentication credentials for No-IP
28
+ """
29
+ if not self.id:
30
+ raise ValueError("No-IP requires username as 'id'")
31
+ if not self.token:
32
+ raise ValueError("No-IP requires password as 'token'")
33
+
34
+ def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
35
+ """
36
+ Update DNS record using No-IP Dynamic Update API
37
+
38
+ No-IP API Reference:
39
+ - URL: https://dynupdate.no-ip.com/nic/update
40
+ - Method: GET or POST
41
+ - Authentication: HTTP Basic Auth (username:password)
42
+ - Parameters:
43
+ - hostname: The hostname to update
44
+ - myip: The IP address to set (optional, uses client IP
45
+ if not provided)
46
+
47
+ Response codes:
48
+ - good: Update successful
49
+ - nochg: IP address is current, no update performed
50
+ - nohost: Hostname supplied does not exist
51
+ - badauth: Invalid username/password combination
52
+ - badagent: Client disabled
53
+ - !donator: An update request was sent including a feature that
54
+ is not available
55
+ - abuse: Username is blocked due to abuse
56
+ """
57
+ self.logger.info("%s => %s(%s)", domain, value, record_type)
58
+
59
+ # Prepare request parameters
60
+ params = {"hostname": domain, "myip": value}
61
+
62
+ # Prepare HTTP Basic Authentication headers
63
+ auth_string = "{0}:{1}".format(self.id, self.token)
64
+ if not isinstance(auth_string, bytes): # Python 3
65
+ auth_bytes = auth_string.encode("utf-8")
66
+ else: # Python 2
67
+ auth_bytes = auth_string
68
+
69
+ auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
70
+ headers = {
71
+ "Authorization": "Basic {0}".format(auth_b64),
72
+ }
73
+
74
+ try:
75
+ # Use GET request as it's the most common method for DDNS
76
+ response = self._http("GET", "/nic/update", queries=params, headers=headers)
77
+
78
+ if response is not None:
79
+ response_str = str(response).strip()
80
+ self.logger.info("No-IP API response: %s", response_str)
81
+
82
+ # Check for successful responses
83
+ if response_str.startswith("good") or response_str.startswith("nochg"):
84
+ return True
85
+ elif response_str.startswith("nohost"):
86
+ self.logger.error("Hostname %s does not exist under No-IP account", domain)
87
+ elif response_str.startswith("badauth"):
88
+ self.logger.error("Invalid No-IP username/password combination")
89
+ elif response_str.startswith("badagent"):
90
+ self.logger.error("No-IP client disabled")
91
+ elif response_str.startswith("!donator"):
92
+ self.logger.error("Feature not available for No-IP free account")
93
+ elif response_str.startswith("abuse"):
94
+ self.logger.error("No-IP account blocked due to abuse")
95
+ else:
96
+ self.logger.error("Unexpected No-IP API response: %s", response_str)
97
+ else:
98
+ self.logger.error("Empty response from No-IP API")
99
+
100
+ except Exception as e:
101
+ self.logger.error("Error updating No-IP record for %s: %s", domain, e)
102
+
103
+ return False
@@ -5,9 +5,9 @@ Tencent Cloud DNSPod API
5
5
 
6
6
  @author: NewFuture
7
7
  """
8
- from ._base import BaseProvider, TYPE_JSON, hmac_sha256_authorization, sha256_hash, hmac_sha256
8
+ from ._base import BaseProvider, TYPE_JSON
9
+ from ._signature import hmac_sha256_authorization, sha256_hash, hmac_sha256
9
10
  from time import time, strftime, gmtime
10
- from json import dumps as jsonencode
11
11
 
12
12
 
13
13
  class TencentCloudProvider(BaseProvider):
@@ -19,7 +19,7 @@ class TencentCloudProvider(BaseProvider):
19
19
  Documentation: https://cloud.tencent.com/document/api/1427
20
20
  """
21
21
 
22
- API = "https://dnspod.tencentcloudapi.com"
22
+ endpoint = "https://dnspod.tencentcloudapi.com"
23
23
  content_type = TYPE_JSON
24
24
 
25
25
  # 腾讯云 DNSPod API 配置
@@ -42,12 +42,12 @@ class TencentCloudProvider(BaseProvider):
42
42
  """
43
43
  # 构建请求体
44
44
  params = {k: v for k, v in params.items() if v is not None}
45
- body = jsonencode(params)
45
+ body = self._encode_body(params)
46
46
 
47
47
  # 构建请求头,小写 腾讯云只签名特定头部
48
48
  headers = {
49
49
  "content-type": self.content_type,
50
- "host": self.API.split("://", 1)[1].strip("/"),
50
+ "host": self.endpoint.split("://", 1)[1].strip("/"),
51
51
  }
52
52
 
53
53
  # 腾讯云特殊的密钥派生过程
@@ -55,13 +55,13 @@ class TencentCloudProvider(BaseProvider):
55
55
  credential_scope = "{}/{}/tc3_request".format(date, self.service)
56
56
 
57
57
  # 派生签名密钥
58
- secret_date = hmac_sha256("TC3" + self.auth_token, date).digest()
58
+ secret_date = hmac_sha256("TC3" + self.token, date).digest()
59
59
  secret_service = hmac_sha256(secret_date, self.service).digest()
60
60
  signing_key = hmac_sha256(secret_service, "tc3_request").digest()
61
61
 
62
62
  # 预处理模板字符串
63
63
  auth_format = "TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
64
- self.auth_id,
64
+ self.id,
65
65
  credential_scope,
66
66
  )
67
67
  timestamp = str(int(time()))
ddns/util/comment.py ADDED
@@ -0,0 +1,88 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ Comment removal utility for JSON configuration files.
4
+ Supports both # and // style single line comments.
5
+ @author: GitHub Copilot
6
+ """
7
+
8
+
9
+ def remove_comment(content):
10
+ # type: (str) -> str
11
+ """
12
+ 移除字符串中的单行注释。
13
+ 支持 # 和 // 两种注释风格。
14
+
15
+ Args:
16
+ content (str): 包含注释的字符串内容
17
+
18
+ Returns:
19
+ str: 移除注释后的字符串
20
+
21
+ Examples:
22
+ >>> remove_comment('{"key": "value"} // comment')
23
+ '{"key": "value"} '
24
+ >>> remove_comment('# This is a comment\\n{"key": "value"}')
25
+ '\\n{"key": "value"}'
26
+ """
27
+ if not content:
28
+ return content
29
+
30
+ lines = content.splitlines()
31
+ cleaned_lines = []
32
+
33
+ for line in lines:
34
+ # 移除行内注释,但要小心不要破坏字符串内的内容
35
+ cleaned_line = _remove_line_comment(line)
36
+ cleaned_lines.append(cleaned_line)
37
+
38
+ return "\n".join(cleaned_lines)
39
+
40
+
41
+ def _remove_line_comment(line):
42
+ # type: (str) -> str
43
+ """
44
+ 移除单行中的注释部分。
45
+
46
+ Args:
47
+ line (str): 要处理的行
48
+
49
+ Returns:
50
+ str: 移除注释后的行
51
+ """
52
+ # 检查是否是整行注释
53
+ stripped = line.lstrip()
54
+ if stripped.startswith("#") or stripped.startswith("//"):
55
+ return ""
56
+
57
+ # 查找行内注释,需要考虑字符串内容
58
+ in_string = False
59
+ quote_char = None
60
+ i = 0
61
+
62
+ while i < len(line):
63
+ char = line[i]
64
+
65
+ # 处理字符串内的转义序列
66
+ if in_string and char == "\\" and i + 1 < len(line):
67
+ i += 2 # 跳过转义字符
68
+ continue
69
+
70
+ # 处理引号字符
71
+ if char in ('"', "'"):
72
+ if not in_string:
73
+ in_string = True
74
+ quote_char = char
75
+ elif char == quote_char:
76
+ in_string = False
77
+ quote_char = None
78
+
79
+ # 在字符串外检查注释标记
80
+ elif not in_string:
81
+ if char == "#":
82
+ return line[:i].rstrip()
83
+ elif char == "/" and i + 1 < len(line) and line[i + 1] == "/":
84
+ return line[:i].rstrip()
85
+
86
+ i += 1
87
+
88
+ return line
ddns/util/http.py CHANGED
@@ -13,13 +13,20 @@ import ssl
13
13
  import os
14
14
 
15
15
  try: # python 3
16
- from http.client import HTTPSConnection, HTTPConnection, HTTPException
17
- from urllib.parse import urlparse
16
+ from urllib.request import Request, HTTPSHandler, ProxyHandler, build_opener, OpenerDirector # noqa: F401
17
+ from urllib.parse import quote, urlencode
18
+ from urllib.error import HTTPError, URLError
18
19
  except ImportError: # python 2
19
- from httplib import HTTPSConnection, HTTPConnection, HTTPException # type: ignore[no-redef]
20
- from urlparse import urlparse # type: ignore[no-redef]
20
+ from urllib2 import Request, HTTPSHandler, ProxyHandler, build_opener, HTTPError, URLError # type: ignore[no-redef]
21
+ from urllib import urlencode, quote # type: ignore[no-redef]
21
22
 
22
- __all__ = ["send_http_request", "HttpResponse"]
23
+ __all__ = [
24
+ "send_http_request",
25
+ "HttpResponse",
26
+ "quote",
27
+ "urlencode",
28
+ "URLError",
29
+ ]
23
30
 
24
31
  logger = getLogger().getChild(__name__)
25
32
 
@@ -28,14 +35,14 @@ class HttpResponse(object):
28
35
  """HTTP响应封装类"""
29
36
 
30
37
  def __init__(self, status, reason, headers, body):
31
- # type: (int, str, list[tuple[str, str]], str) -> None
38
+ # type: (int, str, object, str) -> None
32
39
  """
33
40
  初始化HTTP响应对象
34
41
 
35
42
  Args:
36
43
  status (int): HTTP状态码
37
44
  reason (str): 状态原因短语
38
- headers (list[tuple[str, str]]): 响应头列表,保持原始格式和顺序
45
+ headers (object): 响应头对象,直接使用 response.info()
39
46
  body (str): 响应体内容
40
47
  """
41
48
  self.status = status
@@ -46,51 +53,42 @@ class HttpResponse(object):
46
53
  def get_header(self, name, default=None):
47
54
  # type: (str, str | None) -> str | None
48
55
  """
49
- 获取指定名称的头部值(不区分大小写)
56
+ 获取指定名称的头部值
50
57
 
51
58
  Args:
52
59
  name (str): 头部名称
60
+ default (str | None): 默认值
53
61
 
54
62
  Returns:
55
- str | None: 头部值,如果不存在则返回None
63
+ str | None: 头部值,如果不存在则返回默认值
56
64
  """
57
- name_lower = name.lower()
58
- for header_name, header_value in self.headers:
59
- if header_name.lower() == name_lower:
60
- return header_value
61
- return default
65
+ return self.headers.get(name, default) # type: ignore[union-attr]
62
66
 
63
67
 
64
- def _create_connection(hostname, port, is_https, proxy, verify_ssl):
65
- # type: (str, int | None, bool, str | None, bool | str) -> HTTPConnection | HTTPSConnection
66
- """创建HTTP/HTTPS连接"""
67
- target = proxy or hostname
68
+ # 移除了自定义重定向处理器,使用urllib2/urllib.request的内置重定向处理
68
69
 
69
- if not is_https:
70
- conn = HTTPConnection(target, port)
71
- else:
72
- ssl_context = ssl.create_default_context()
73
-
74
- if verify_ssl is False:
75
- # 禁用SSL验证
76
- ssl_context.check_hostname = False
77
- ssl_context.verify_mode = ssl.CERT_NONE
78
- elif hasattr(verify_ssl, "lower") and verify_ssl.lower() not in ("auto", "true"): # type: ignore[union-attr]
79
- # 使用自定义CA证书 lower 判断 str/unicode 兼容 python2
80
- try:
81
- ssl_context.load_verify_locations(verify_ssl) # type: ignore[arg-type]
82
- except Exception as e:
83
- logger.error("Failed to load CA certificate from %s: %s", verify_ssl, e)
84
- else:
85
- # 默认验证,尝试加载系统证书
86
- _load_system_ca_certs(ssl_context)
87
- conn = HTTPSConnection(target, port, context=ssl_context)
88
70
 
89
- # 设置代理隧道
90
- if proxy:
91
- conn.set_tunnel(hostname, port) # type: ignore[attr-defined]
71
+ def _create_ssl_context(verify_ssl):
72
+ # type: (bool | str) -> ssl.SSLContext | None
73
+ """创建SSL上下文"""
74
+ ssl_context = ssl.create_default_context()
92
75
 
93
- return conn
76
+ if verify_ssl is False:
77
+ # 禁用SSL验证
78
+ ssl_context.check_hostname = False
79
+ ssl_context.verify_mode = ssl.CERT_NONE
80
+ elif hasattr(verify_ssl, "lower") and verify_ssl.lower() not in ("auto", "true"): # type: ignore[union-attr]
81
+ # 使用自定义CA证书
82
+ try:
83
+ ssl_context.load_verify_locations(verify_ssl) # type: ignore[arg-type]
84
+ except Exception as e:
85
+ logger.error("Failed to load CA certificate from %s: %s", verify_ssl, e)
86
+ return None
87
+ else:
88
+ # 默认验证,尝试加载系统证书
89
+ _load_system_ca_certs(ssl_context)
90
+
91
+ return ssl_context
94
92
 
95
93
 
96
94
  def _load_system_ca_certs(ssl_context):
@@ -123,125 +121,78 @@ def _load_system_ca_certs(ssl_context):
123
121
  loaded_count += 1
124
122
  logger.debug("Loaded CA certificates from: %s", ca_path)
125
123
  except Exception as e:
126
- logger.debug("Failed to load CA certificates from %s: %s", ca_path, e)
124
+ logger.info("Failed to load CA certificates from %s: %s", ca_path, e)
127
125
 
128
- if loaded_count > 0:
129
- logger.debug("Successfully loaded CA certificates from %d locations", loaded_count)
130
126
 
127
+ def _create_opener(proxy, verify_ssl):
128
+ # type: (str | None, bool | str) -> OpenerDirector
129
+ """创建URL打开器,支持代理和SSL配置"""
130
+ handlers = []
131
131
 
132
- def _close_connection(conn):
133
- # type: (HTTPConnection | HTTPSConnection) -> None
134
- """关闭HTTP/HTTPS连接"""
135
- try:
136
- conn.close()
137
- except Exception as e:
138
- logger.warning("Failed to close connection: %s", e)
132
+ if proxy:
133
+ handlers.append(ProxyHandler({"http": proxy, "https": proxy}))
134
+
135
+ ssl_context = _create_ssl_context(verify_ssl)
136
+ if ssl_context:
137
+ handlers.append(HTTPSHandler(context=ssl_context))
138
+
139
+ return build_opener(*handlers)
139
140
 
140
141
 
141
- def send_http_request(method, url, body=None, headers=None, proxy=None, max_redirects=5, verify_ssl=True):
142
- # type: (str, str, str | bytes | None, dict[str, str] | None, str | None, int, bool | str) -> HttpResponse
142
+ def send_http_request(method, url, body=None, headers=None, proxy=None, verify_ssl=True):
143
+ # type: (str, str, str | bytes | None, dict[str, str] | None, str | None, bool | str) -> HttpResponse
143
144
  """
144
145
  发送HTTP/HTTPS请求,支持重定向跟随和灵活的SSL验证
145
- Send HTTP/HTTPS request with support for redirect following and flexible SSL verification.
146
+
146
147
  Args:
147
148
  method (str): HTTP方法,如GET、POST等
148
149
  url (str): 请求的URL
149
150
  body (str | bytes | None): 请求体
150
151
  headers (dict[str, str] | None): 请求头
151
152
  proxy (str | None): 代理地址
152
- max_redirects (int): 最大重定向次数
153
- verify_ssl (bool | str): 是否验证SSL证书
153
+ verify_ssl (bool | str): SSL验证配置
154
+
154
155
  Returns:
155
- HttpResponse: 响应对象,包含状态码、头部和解码后的内容
156
+ HttpResponse: 响应对象
157
+
156
158
  Raises:
157
- HTTPException: 如果请求失败或重定向次数超过限制
159
+ URLError: 如果请求失败
158
160
  ssl.SSLError: 如果SSL验证失败
159
161
  """
160
- if max_redirects <= 0:
161
- raise HTTPException("Too many redirects")
162
+ # 准备请求
163
+ if isinstance(body, str):
164
+ body = body.encode("utf-8")
165
+
166
+ req = Request(url, data=body)
167
+ req.get_method = lambda: method # type: ignore[attr-defined]
162
168
 
163
- # 解析URL
164
- url_obj = urlparse(url)
165
- is_https = url_obj.scheme == "https"
166
- hostname = url_obj.hostname or url_obj.netloc.split(":")[0]
167
- request_path = "{}?{}".format(url_obj.path, url_obj.query) if url_obj.query else url_obj.path
168
- headers = headers or {}
169
+ if headers:
170
+ for key, value in headers.items():
171
+ req.add_header(key, value)
169
172
 
170
- # 创建连接
171
- actual_verify_ssl = verify_ssl
172
- conn = _create_connection(hostname, url_obj.port, is_https, proxy, verify_ssl)
173
+ # 创建opener并发送请求
174
+ opener = _create_opener(proxy, verify_ssl)
173
175
 
174
- # 执行请求,处理SSL错误
175
176
  try:
176
- conn.request(method, request_path, body, headers)
177
- response = conn.getresponse()
177
+ response = opener.open(req)
178
+ response_headers = response.info()
179
+ raw_body = response.read()
180
+ decoded_body = _decode_response_body(raw_body, response_headers.get("Content-Type"))
181
+ return HttpResponse(response.getcode(), getattr(response, "msg", ""), response_headers, decoded_body)
182
+ except HTTPError as e:
183
+ # 记录HTTP错误并读取响应体用于调试
184
+ response_headers = getattr(e, "headers", {})
185
+ raw_body = e.read()
186
+ decoded_body = _decode_response_body(raw_body, response_headers.get("Content-Type"))
187
+ logger.error("HTTP error %s: %s for %s", e.code, getattr(e, "reason", str(e)), url)
188
+ return HttpResponse(e.code, getattr(e, "reason", str(e)), response_headers, decoded_body)
178
189
  except ssl.SSLError:
179
- _close_connection(conn)
180
- if verify_ssl == "auto" and is_https:
190
+ if verify_ssl == "auto":
181
191
  logger.warning("SSL verification failed, switching to unverified connection %s", url)
182
- # 重新连接,忽略SSL验证
183
- conn = _create_connection(hostname, url_obj.port, is_https, proxy, False)
184
- conn.request(method, request_path, body, headers)
185
- response = conn.getresponse()
186
- actual_verify_ssl = False
192
+ return send_http_request(method, url, body, headers, proxy, False)
187
193
  else:
188
194
  raise
189
195
 
190
- # 检查重定向
191
- status = response.status
192
- if 300 <= status < 400:
193
- location = response.getheader("Location")
194
- _close_connection(conn)
195
- if not location:
196
- # 无Location头的重定向
197
- logger.warning("Redirect status %d but no Location header", status)
198
- location = ""
199
-
200
- # 构建重定向URL
201
- redirect_url = _build_redirect_url(location, "{}://{}".format(url_obj.scheme, url_obj.netloc), url_obj.path)
202
-
203
- # 如果重定向URL没有查询字符串,但原始URL有,则附加
204
- if url_obj.query and "?" not in redirect_url:
205
- redirect_url += "?" + url_obj.query
206
-
207
- # 确定重定向方法:303或302+POST转为GET,其他保持原方法
208
- if status == 303 or (status == 302 and method == "POST"):
209
- method, body = "GET", None
210
- # 如果从POST转为GET,移除相关的头部
211
- if headers:
212
- headers = {k: v for k, v in headers.items() if k.lower() not in ("content-length", "content-type")}
213
-
214
- logger.info("Redirecting [%d] to: %s", status, redirect_url)
215
- # 递归处理重定向
216
- return send_http_request(method, redirect_url, body, headers, proxy, max_redirects - 1, actual_verify_ssl)
217
-
218
- # 处理最终响应
219
- content_type = response.getheader("Content-Type")
220
- response_headers = response.getheaders()
221
- raw_body = response.read()
222
- _close_connection(conn)
223
-
224
- # 解码响应体并创建响应对象
225
- decoded_body = _decode_response_body(raw_body, content_type)
226
- return HttpResponse(status, response.reason, response_headers, decoded_body)
227
-
228
-
229
- def _build_redirect_url(location, base, path):
230
- # type: (str, str, str) -> str
231
- """构建重定向URL,使用简单的字符串操作"""
232
- if location.startswith("http"):
233
- return location
234
-
235
- if location.startswith("/"):
236
- # 绝对路径:使用base的scheme和netloc
237
- base_url = urlparse(base)
238
- return "{}://{}{}".format(base_url.scheme, base_url.netloc, location)
239
- else:
240
- base_path = path.rsplit("/", 1)[0] if "/" in path else ""
241
- if not base_path.endswith("/"):
242
- base_path += "/"
243
- return base + base_path + location
244
-
245
196
 
246
197
  def _decode_response_body(raw_body, content_type):
247
198
  # type: (bytes, str | None) -> str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddns
3
- Version: 4.1.0b1
3
+ Version: 4.1.0b2
4
4
  Summary: Dynamic DNS client for multiple providers, supporting IPv4 and IPv6.
5
5
  Author-email: NewFuture <python@newfuture.cc>
6
6
  License-Expression: MIT
@@ -84,12 +84,16 @@ Dynamic: license-file
84
84
  - 服务商支持:
85
85
  - [DNSPOD](https://www.dnspod.cn/) ([配置指南](doc/providers/dnspod.md))
86
86
  - [阿里 DNS](http://www.alidns.com/) ([配置指南](doc/providers/alidns.md)) ⚡
87
- - [DNS.COM](https://www.dns.com/) (@loftor-git)
88
- - [DNSPOD 国际版](https://www.dnspod.com/)
89
- - [CloudFlare](https://www.cloudflare.com/) (@tongyifan)
90
- - [HE.net](https://dns.he.net/) (@NN708) (不支持自动创建记录)
91
- - [华为云](https://huaweicloud.com/) (@cybmp3)
87
+ - [阿里云边缘安全加速(ESA)](https://esa.console.aliyun.com/) ([配置指南](doc/providers/aliesa.md)) ⚡
88
+ - [DNS.COM](https://www.dns.com/) ([配置指南](doc/providers/51dns.md)) (@loftor-git)
89
+ - [DNSPOD 国际版](https://www.dnspod.com/) ([配置指南](doc/providers/dnspod_com.md))
90
+ - [CloudFlare](https://www.cloudflare.com/) ([配置指南](doc/providers/cloudflare.md)) (@tongyifan)
91
+ - [HE.net](https://dns.he.net/) ([配置指南](doc/providers/he.md)) (@NN708) (不支持自动创建记录)
92
+ - [华为云](https://huaweicloud.com/) ([配置指南](doc/providers/huaweidns.md)) (@cybmp3) ⚡
93
+ - [NameSilo](https://www.namesilo.com/) ([配置指南](doc/providers/namesilo.md))
92
94
  - [腾讯云](https://cloud.tencent.com/) ([配置指南](doc/providers/tencentcloud.md)) ⚡
95
+ - [腾讯云 EdgeOne](https://cloud.tencent.com/product/teo) ([配置指南](doc/providers/edgeone.md)) ⚡
96
+ - [No-IP](https://www.noip.com/) ([配置指南](doc/providers/noip.md))
93
97
  - 自定义回调 API ([配置指南](doc/providers/callback.md))
94
98
 
95
99
  > ⚡ 标记的服务商使用高级 HMAC-SHA256 签名认证,提供企业级安全保障
@@ -161,12 +165,16 @@ Dynamic: license-file
161
165
 
162
166
  - **DNSPOD(中国版)**: [创建 token](https://support.dnspod.cn/Kb/showarticle/tsid/227/) | [详细配置文档](doc/providers/dnspod.md)
163
167
  - **阿里云 DNS**: [申请 accesskey](https://help.aliyun.com/document_detail/87745.htm) | [详细配置文档](doc/providers/alidns.md)
164
- - **DNS.COM**: [API Key/Secret](https://www.dns.com/member/apiSet)
165
- - **DNSPOD(国际版)**: [获取 token](https://www.dnspod.com/docs/info.html#get-the-user-token)
166
- - **CloudFlare**: [API Key](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-)(除了 `email + API KEY`,也可使用 `Token`,**需要list Zone 权限**)
167
- - **HE.net**: [DDNS 文档](https://dns.he.net/docs.html)(仅需将设置的密码填入 `token` 字段,`id` 字段可留空)
168
- - **华为云 DNS**: [APIKEY 申请](https://console.huaweicloud.com/iam/)(点左边访问密钥,然后点新增访问密钥)
168
+ - **阿里云边缘安全加速(ESA)**: [申请 accesskey](https://help.aliyun.com/document_detail/87745.htm) | [详细配置文档](doc/providers/aliesa.md)
169
+ - **51DNS(dns.com)**: [API Key/Secret](https://www.dns.com/member/apiSet) | [详细配置文档](doc/providers/51dns.md)
170
+ - **DNSPOD(国际版)**: [获取 token](https://www.dnspod.com/docs/info.html#get-the-user-token) | [详细配置文档](doc/providers/dnspod_com.md)
171
+ - **CloudFlare**: [API Key](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-)(除了 `email + API KEY`,也可使用 `Token`,**需要list Zone 权限**) | [详细配置文档](doc/providers/cloudflare.md)
172
+ - **HE.net**: [DDNS 文档](https://dns.he.net/docs.html)(仅需将设置的密码填入 `token` 字段,`id` 字段可留空) | [详细配置文档](doc/providers/he.md)
173
+ - **华为云 DNS**: [APIKEY 申请](https://console.huaweicloud.com/iam/)(点左边访问密钥,然后点新增访问密钥) | [详细配置文档](doc/providers/huaweidns.md)
174
+ - **NameSilo**: [API Key](https://www.namesilo.com/account/api-manager)(API Manager 中获取 API Key) | [详细配置文档](doc/providers/namesilo.md)
169
175
  - **腾讯云 DNS**: [详细配置文档](doc/providers/tencentcloud.md)
176
+ - **腾讯云 EdgeOne**: [详细配置文档](doc/providers/edgeone.md)
177
+ - **No-IP**: [用户名和密码](https://www.noip.com/)(使用 No-IP 账户的用户名和密码) | [详细配置文档](doc/providers/noip.md)
170
178
  - **自定义回调**: 参数填写方式请查看下方的自定义回调配置说明
171
179
 
172
180
  2. 修改配置文件,`ipv4` 和 `ipv6` 字段,为待更新的域名,详细参照配置说明
@@ -217,7 +225,7 @@ python -m ddns -c /path/to/config.json
217
225
  | :----: | :----------------: | :------: | :---------: | :----------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
218
226
  | id | string | √ | 无 | api 访问 ID | Cloudflare 为邮箱(使用 Token 时留空)<br>HE.net 可留空<br>华为云为 Access Key ID (AK) |
219
227
  | token | string | √ | 无 | api 授权 token | 部分平台叫 secret key,**反馈粘贴时删除** |
220
- | dns | string | No | `"dnspod"` | dns 服务商 | 阿里 DNS 为 `alidns`,Cloudflare 为 `cloudflare`,dns.com 为 `dnscom`,DNSPOD 国内为 `dnspod`,DNSPOD 国际为 `dnspod_com`,HE.net 为 `he`,华为云为 `huaweidns`,腾讯云为 `tencentcloud`,自定义回调为 `callback`。部分服务商有[详细配置文档](doc/providers/) |
228
+ | dns | string | No | `"dnspod"` | dns 服务商 | 阿里 DNS 为 `alidns`,阿里ESA为 `aliesa`,Cloudflare 为 `cloudflare`,dns.com 为 `dnscom`,DNSPOD 国内为 `dnspod`,DNSPOD 国际为 `dnspod_com`,HE.net 为 `he`,华为云为 `huaweidns`,NameSilo 为 `namesilo`,腾讯云为 `tencentcloud`,腾讯云EdgeOne为 `edgeone`,No-IP 为 `noip`,自定义回调为 `callback`。部分服务商有[详细配置文档](doc/providers/) |
221
229
  | ipv4 | array | No | `[]` | ipv4 域名列表 | 为 `[]` 时,不会获取和更新 IPv4 地址 |
222
230
  | ipv6 | array | No | `[]` | ipv6 域名列表 | 为 `[]` 时,不会获取和更新 IPv6 地址 |
223
231
  | index4 | string\|int\|array | No | `"default"` | ipv4 获取方式 | 可设置 `网卡`、`内网`、`公网`、`正则` 等方式 |
@@ -266,7 +274,7 @@ python -m ddns -c /path/to/config.json
266
274
  "$schema": "https://ddns.newfuture.cc/schema/v4.0.json",
267
275
  "id": "12345",
268
276
  "token": "mytokenkey",
269
- "dns": "dnspod 或 dnspod_com 或 alidns 或 dnscom 或 cloudflare 或 he 或 huaweidns 或 tencentcloud 或 callback",
277
+ "dns": "dnspod 或 dnspod_com 或 alidns 或 aliesa 或 dnscom 或 cloudflare 或 he 或 huaweidns 或 namesilo 或 tencentcloud 或 noip 或 callback",
270
278
  "ipv4": ["ddns.newfuture.cc", "ipv4.ddns.newfuture.cc"],
271
279
  "ipv6": ["ddns.newfuture.cc", "ipv6.ddns.newfuture.cc"],
272
280
  "index4": 0,
@@ -330,6 +338,7 @@ Docker 镜像在无额外参数的情况下,已默认启用每 5 分钟执行
330
338
  使用系统自带的 IE 浏览器访问一次对应的 API 即可
331
339
 
332
340
  - alidns 打开: <https://alidns.aliyuncs.com>
341
+ - aliesa 打开: <https://esa.cn-hangzhou.aliyuncs.com>
333
342
  - cloudflare 打开: <https://api.cloudflare.com>
334
343
  - dns.com 打开: <https://www.dns.com>
335
344
  - dnspod.cn 打开: <https://dnsapi.cn>
@@ -0,0 +1,31 @@
1
+ ddns/__builtins__.pyi,sha256=QTTtckbWAAZasu-uzeeyK4MLj9S9dxLtRlhty6U01jQ,109
2
+ ddns/__init__.py,sha256=SUqnkj9QXJFuc7vXN63wohdxYu2OupGgfTyZYYtO-uI,258
3
+ ddns/__main__.py,sha256=PrkwVahIUGvT9CNy3uXs8ZX-ox2RVx72aeWexhl6YZM,4134
4
+ ddns/cache.py,sha256=A1s3rnOJbPrGEjbGbbISrVg46lZFzZ84WPdzHaCtYBk,5971
5
+ ddns/ip.py,sha256=K4iFWwBX9kSrULVdNGX17dc_Zbqy9VpmpP2mdU_h6Cc,3947
6
+ ddns/provider/__init__.py,sha256=qAw-R-l7nUA9L96Tmr2n-T7_g8T9Mah7u615yg2zScY,2793
7
+ ddns/provider/_base.py,sha256=X73ID1iyz11zCX2nQne-8i7xW5CPGIXuWSs2zFI_Al8,20810
8
+ ddns/provider/_signature.py,sha256=fF8XxMDkXjia96d1gIVYWc72MVVe65AAnD7qr51qKXA,4140
9
+ ddns/provider/alidns.py,sha256=sBwDrtARbqgfEynPyR-dVfAVeh4K6lhO1XL9_0C1Tig,6096
10
+ ddns/provider/aliesa.py,sha256=5tGPyW22TtI7AGJKugIOipby3R68bHHEJiR_k9og1Rs,4695
11
+ ddns/provider/callback.py,sha256=WSd8QNpWUUJXQbyIatANmyigLU6VBvrLBsn7lALz8yA,2924
12
+ ddns/provider/cloudflare.py,sha256=0sVelYFU3ndjREQjtQAaT6G3jxj1Stk3hN7bT_NAUpI,4484
13
+ ddns/provider/debug.py,sha256=3TZYcXHcIzTwUEj5_rvZTT0QqS6jFwTuBS7cMujG2tM,587
14
+ ddns/provider/dnscom.py,sha256=IfdX_HaEjH12NHxdv6taRiX-zLQTo4kpBrJ1VWE9au4,3667
15
+ ddns/provider/dnspod.py,sha256=CSAIJYIjna-PphR4KE9KlMsPcgfEyRnJPCIEDqYlzNg,4445
16
+ ddns/provider/dnspod_com.py,sha256=uTFBSJIlFAijbpXYvte_pQHpGvTR4co9mEkJ0prRq7s,398
17
+ ddns/provider/edgeone.py,sha256=ck8GkkOk-Wa01QoGoPfD2pr6ok7gLuQyB7TDsSyNtHE,3897
18
+ ddns/provider/he.py,sha256=v6x_2_iJBr1RMefaBgnn3TWNk8-JxcL3llxmxr1vz1w,1820
19
+ ddns/provider/huaweidns.py,sha256=LH6EiCzGuftg3dNk7ZiPLaS6BvM0vWydrRXhtaLxj04,5590
20
+ ddns/provider/namesilo.py,sha256=jgqkQUcHVCyVjYoxazmcN9Gu_Zd-EAv3aSnNmHHJ1jk,5843
21
+ ddns/provider/noip.py,sha256=JY2939xCu0hF6wl1QPZpxz-2TgwYVY0GWMY97iRVHME,3961
22
+ ddns/provider/tencentcloud.py,sha256=SzQlTVU9W3NRq4QvK88eP4A3y6g13M0Ud-AE93qvSQ8,7153
23
+ ddns/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ ddns/util/comment.py,sha256=_B8sRpCJksNspsdld4ha03W1TAi-MyPRK7pL7ym0j9k,2199
25
+ ddns/util/http.py,sha256=xG5QyRO8r-pCnF4YBfGs_UxqUamX_f3n_wshw6FlMqM,7664
26
+ ddns-4.1.0b2.dist-info/licenses/LICENSE,sha256=MI-ECjp-Vl7WZLiSPY6r5VwrOReNiICVB1QCXiUGt_s,1111
27
+ ddns-4.1.0b2.dist-info/METADATA,sha256=bwPvJGxEL0JEahaLk3PD4gI04tZdVUpAJQQMKH0xP68,20807
28
+ ddns-4.1.0b2.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
29
+ ddns-4.1.0b2.dist-info/entry_points.txt,sha256=2-VbA-WZcjebkZrGKvUCuBBRYF4xQNMoLIoGaS234WU,44
30
+ ddns-4.1.0b2.dist-info/top_level.txt,sha256=Se0wn3T8Bc4pj55dGwVrCe8BFwmFCBwQVHF1bTyV0o0,5
31
+ ddns-4.1.0b2.dist-info/RECORD,,