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 +5 -0
- ddns/__init__.py +2 -9
- ddns/__main__.py +77 -146
- ddns/cache.py +183 -0
- ddns/{util/ip.py → ip.py} +26 -21
- ddns/provider/__init__.py +79 -0
- ddns/provider/_base.py +539 -0
- ddns/provider/_signature.py +113 -0
- ddns/provider/alidns.py +139 -171
- ddns/provider/aliesa.py +129 -0
- ddns/provider/callback.py +66 -105
- ddns/provider/cloudflare.py +87 -151
- ddns/provider/debug.py +20 -0
- ddns/provider/dnscom.py +91 -169
- ddns/provider/dnspod.py +104 -174
- ddns/provider/dnspod_com.py +11 -8
- ddns/provider/edgeone.py +82 -0
- ddns/provider/he.py +41 -78
- ddns/provider/huaweidns.py +133 -256
- ddns/provider/namesilo.py +159 -0
- ddns/provider/noip.py +103 -0
- ddns/provider/tencentcloud.py +195 -0
- ddns/util/comment.py +88 -0
- ddns/util/http.py +228 -0
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/METADATA +109 -75
- ddns-4.1.0b2.dist-info/RECORD +31 -0
- ddns/util/cache.py +0 -139
- ddns/util/config.py +0 -317
- ddns-4.0.2.dist-info/RECORD +0 -21
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/WHEEL +0 -0
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/entry_points.txt +0 -0
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.0.2.dist-info → ddns-4.1.0b2.dist-info}/top_level.txt +0 -0
ddns/provider/huaweidns.py
CHANGED
|
@@ -2,264 +2,141 @@
|
|
|
2
2
|
"""
|
|
3
3
|
HuaweiDNS API
|
|
4
4
|
华为DNS解析操作库
|
|
5
|
-
|
|
6
|
-
@author: cybmp3
|
|
5
|
+
@author: NewFuture
|
|
7
6
|
"""
|
|
8
7
|
|
|
8
|
+
from ._base import BaseProvider, TYPE_JSON, join_domain, encode_params
|
|
9
|
+
from ._signature import hmac_sha256_authorization, sha256_hash
|
|
10
|
+
from time import strftime, gmtime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HuaweiDNSProvider(BaseProvider):
|
|
14
|
+
endpoint = "https://dns.myhuaweicloud.com"
|
|
15
|
+
content_type = TYPE_JSON
|
|
16
|
+
algorithm = "SDK-HMAC-SHA256"
|
|
17
|
+
|
|
18
|
+
def _validate(self):
|
|
19
|
+
self.logger.warning(
|
|
20
|
+
"华为云 DNS 缺少充分的真实环境测试,请及时在 GitHub Issues 中反馈: %s",
|
|
21
|
+
"https://github.com/NewFuture/DDNS/issues",
|
|
22
|
+
)
|
|
23
|
+
super(HuaweiDNSProvider, self)._validate()
|
|
24
|
+
|
|
25
|
+
def _request(self, method, path, **params):
|
|
26
|
+
"""
|
|
27
|
+
https://support.huaweicloud.com/api-dns/zh-cn_topic_0037134406.html
|
|
28
|
+
https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-002.html
|
|
29
|
+
https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-003.html
|
|
30
|
+
https://support.huaweicloud.com/devg-apisign/api-sign-algorithm-004.html
|
|
31
|
+
"""
|
|
32
|
+
# type: (str, str, **Any) -> dict
|
|
33
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
34
|
+
if method.upper() == "GET" or method.upper() == "DELETE":
|
|
35
|
+
query = encode_params(params)
|
|
36
|
+
body = ""
|
|
37
|
+
else:
|
|
38
|
+
query = ""
|
|
39
|
+
body = self._encode_body(params)
|
|
40
|
+
|
|
41
|
+
now = strftime("%Y%m%dT%H%M%SZ", gmtime())
|
|
42
|
+
headers = {
|
|
43
|
+
"content-type": self.content_type,
|
|
44
|
+
"host": self.endpoint.split("://", 1)[1].strip("/"),
|
|
45
|
+
"X-Sdk-Date": now,
|
|
46
|
+
}
|
|
9
47
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class Config:
|
|
34
|
-
ID = "id" # AK
|
|
35
|
-
TOKEN = "TOKEN" # AS
|
|
36
|
-
PROXY = None # 代理设置
|
|
37
|
-
TTL = None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class API:
|
|
41
|
-
# API 配置
|
|
42
|
-
SCHEME = 'https'
|
|
43
|
-
SITE = 'dns.myhuaweicloud.com' # API endpoint
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def HexEncodeSHA256Hash(data):
|
|
47
|
-
sha = sha256()
|
|
48
|
-
sha.update(data)
|
|
49
|
-
return sha.hexdigest()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def StringToSign(canonical_request, t):
|
|
53
|
-
b = HexEncodeSHA256Hash(canonical_request)
|
|
54
|
-
return "%s\n%s\n%s" % (Algorithm, datetime.strftime(t, BasicDateFormat), b)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def CanonicalHeaders(headers, signed_headers):
|
|
58
|
-
a = []
|
|
59
|
-
__headers = {}
|
|
60
|
-
for key in headers:
|
|
61
|
-
key_encoded = key.lower()
|
|
62
|
-
value = headers[key]
|
|
63
|
-
value_encoded = value.strip()
|
|
64
|
-
__headers[key_encoded] = value_encoded
|
|
65
|
-
for key in signed_headers:
|
|
66
|
-
a.append(key + ":" + __headers[key])
|
|
67
|
-
return '\n'.join(a) + "\n"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def request(method, path, param=None, body=None, **params):
|
|
71
|
-
# path 是不带host但是 前面需要带 / , body json 字符串或者自己从dict转换下
|
|
72
|
-
# 也可以自己改成 判断下是不是post 是post params就是body
|
|
73
|
-
if param:
|
|
74
|
-
params.update(param)
|
|
75
|
-
|
|
76
|
-
query = urlencode(sorted(params.items()))
|
|
77
|
-
headers = {"content-type": "application/json"} # 初始化header
|
|
78
|
-
headers["X-Sdk-Date"] = datetime.strftime(
|
|
79
|
-
datetime.utcnow(), BasicDateFormat)
|
|
80
|
-
headers["host"] = API.SITE
|
|
81
|
-
# 如何后来有需要把header头 key转换为小写 value 删除前导空格和尾随空格
|
|
82
|
-
sign_headers = []
|
|
83
|
-
for key in headers:
|
|
84
|
-
sign_headers.append(key.lower())
|
|
85
|
-
# 先排序
|
|
86
|
-
sign_headers.sort()
|
|
87
|
-
|
|
88
|
-
if body is None:
|
|
89
|
-
body = ""
|
|
90
|
-
|
|
91
|
-
hex_encode = HexEncodeSHA256Hash(body.encode('utf-8'))
|
|
92
|
-
# 生成文档中的CanonicalRequest
|
|
93
|
-
canonical_headers = CanonicalHeaders(headers, sign_headers)
|
|
94
|
-
|
|
95
|
-
# 签名中的path 必须 / 结尾
|
|
96
|
-
if path[-1] != '/':
|
|
97
|
-
sign_path = path + "/"
|
|
98
|
-
else:
|
|
99
|
-
sign_path = path
|
|
100
|
-
|
|
101
|
-
canonical_request = "%s\n%s\n%s\n%s\n%s\n%s" % (method.upper(), sign_path, query,
|
|
102
|
-
canonical_headers, ";".join(sign_headers), hex_encode)
|
|
103
|
-
|
|
104
|
-
hashed_canonical_request = HexEncodeSHA256Hash(
|
|
105
|
-
canonical_request.encode('utf-8'))
|
|
106
|
-
|
|
107
|
-
# StringToSign
|
|
108
|
-
str_to_sign = "%s\n%s\n%s" % (
|
|
109
|
-
Algorithm, headers['X-Sdk-Date'], hashed_canonical_request)
|
|
110
|
-
|
|
111
|
-
secret = Config.TOKEN
|
|
112
|
-
# 计算签名 HexEncode(HMAC(Access Secret Key, string to sign))
|
|
113
|
-
signature = hmac(secret.encode(
|
|
114
|
-
'utf-8'), str_to_sign.encode('utf-8'), digestmod=sha256).digest()
|
|
115
|
-
signature = hexlify(signature).decode()
|
|
116
|
-
# 添加签名信息到请求头
|
|
117
|
-
auth_header = "%s Access=%s, SignedHeaders=%s, Signature=%s" % (
|
|
118
|
-
Algorithm, Config.ID, ";".join(sign_headers), signature)
|
|
119
|
-
headers['Authorization'] = auth_header
|
|
120
|
-
# 创建Http请求
|
|
121
|
-
|
|
122
|
-
if Config.PROXY:
|
|
123
|
-
conn = HTTPSConnection(Config.PROXY)
|
|
124
|
-
conn.set_tunnel(API.SITE, 443)
|
|
125
|
-
else:
|
|
126
|
-
conn = HTTPSConnection(API.SITE)
|
|
127
|
-
conn.request(method, API.SCHEME + "://" + API.SITE +
|
|
128
|
-
path + '?' + query, body, headers)
|
|
129
|
-
info(API.SCHEME + "://" + API.SITE + path + '?' + query, body)
|
|
130
|
-
resp = conn.getresponse()
|
|
131
|
-
data = resp.read().decode('utf8')
|
|
132
|
-
resp.close()
|
|
133
|
-
if resp.status < 200 or resp.status >= 300:
|
|
134
|
-
|
|
135
|
-
warning('%s : error[%d]: %s', path, resp.status, data)
|
|
136
|
-
raise Exception(data)
|
|
137
|
-
else:
|
|
138
|
-
data = jsondecode(data)
|
|
139
|
-
debug('%s : result:%s', path, data)
|
|
48
|
+
# 使用通用签名函数
|
|
49
|
+
body_hash = sha256_hash(body)
|
|
50
|
+
# 华为云需要在签名时使用带尾斜杠的路径
|
|
51
|
+
sign_path = path if path.endswith("/") else path + "/"
|
|
52
|
+
authorization_format = "%s Access=%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
|
|
53
|
+
self.algorithm,
|
|
54
|
+
self.id,
|
|
55
|
+
)
|
|
56
|
+
authorization = hmac_sha256_authorization(
|
|
57
|
+
secret_key=self.token,
|
|
58
|
+
method=method,
|
|
59
|
+
path=sign_path,
|
|
60
|
+
query=query,
|
|
61
|
+
headers=headers,
|
|
62
|
+
body_hash=body_hash,
|
|
63
|
+
signing_string_format=self.algorithm + "\n" + now + "\n{HashedCanonicalRequest}",
|
|
64
|
+
authorization_format=authorization_format,
|
|
65
|
+
)
|
|
66
|
+
headers["Authorization"] = authorization
|
|
67
|
+
|
|
68
|
+
# 使用原始路径发送实际请求
|
|
69
|
+
path = "{}?{}".format(path, query) if query else path
|
|
70
|
+
data = self._http(method, path, headers=headers, body=body)
|
|
140
71
|
return data
|
|
141
72
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if records: # update
|
|
213
|
-
for (rid, record) in records.items():
|
|
214
|
-
if record['records'] != value:
|
|
215
|
-
"""
|
|
216
|
-
PUT https://{endpoint}/v2/zones/{zone_id}/recordsets/{recordset_id}
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
"name" : "www.example.com.",
|
|
220
|
-
"description" : "This is an example record set.",
|
|
221
|
-
"type" : "A",
|
|
222
|
-
"ttl" : 3600,
|
|
223
|
-
"records" : [ "192.168.10.1", "192.168.10.2" ]
|
|
224
|
-
}
|
|
225
|
-
"""
|
|
226
|
-
body = {
|
|
227
|
-
"name": domain,
|
|
228
|
-
"description": "Managed by DDNS.",
|
|
229
|
-
"type": record_type,
|
|
230
|
-
"records": [
|
|
231
|
-
value
|
|
232
|
-
]
|
|
233
|
-
}
|
|
234
|
-
# 如果TTL不为空,则添加到字典中
|
|
235
|
-
if Config.TTL is not None:
|
|
236
|
-
body['ttl'] = Config.TTL
|
|
237
|
-
res = request('PUT', '/v2/zones/' + zoneid + '/recordsets/' + record['id'],
|
|
238
|
-
body=str(jsonencode(body)))
|
|
239
|
-
if res:
|
|
240
|
-
get_records.records[cache_key][rid]['records'] = value
|
|
241
|
-
result[rid] = res.get("name")
|
|
242
|
-
else:
|
|
243
|
-
result[rid] = "Update fail!\n" + str(res)
|
|
244
|
-
else:
|
|
245
|
-
result[rid] = domain
|
|
246
|
-
else: # create
|
|
247
|
-
body = {
|
|
248
|
-
"name": domain,
|
|
249
|
-
"description": "Managed by DDNS.",
|
|
250
|
-
"type": record_type,
|
|
251
|
-
"records": [
|
|
252
|
-
value
|
|
253
|
-
]
|
|
254
|
-
}
|
|
255
|
-
# 如果TTL不为空,则添加到字典中
|
|
256
|
-
if Config.TTL is not None:
|
|
257
|
-
body['ttl'] = Config.TTL
|
|
258
|
-
res = request('POST', '/v2/zones/' + zoneid + '/recordsets',
|
|
259
|
-
body=str(jsonencode(body)))
|
|
260
|
-
if res:
|
|
261
|
-
get_records.records[cache_key][res['id']] = res
|
|
262
|
-
result = res
|
|
263
|
-
else:
|
|
264
|
-
result = domain + " created fail!"
|
|
265
|
-
return result
|
|
73
|
+
def _query_zone_id(self, domain):
|
|
74
|
+
"""https://support.huaweicloud.com/api-dns/dns_api_62003.html"""
|
|
75
|
+
domain = domain + "." if not domain.endswith(".") else domain
|
|
76
|
+
data = self._request("GET", "/v2/zones", search_mode="equal", limit=500, name=domain)
|
|
77
|
+
zones = data.get("zones", [])
|
|
78
|
+
zone = next((z for z in zones if domain == z.get("name")), None)
|
|
79
|
+
zoneid = zone and zone["id"]
|
|
80
|
+
return zoneid
|
|
81
|
+
|
|
82
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
83
|
+
"""
|
|
84
|
+
v2.1 https://support.huaweicloud.com/api-dns/dns_api_64004.html
|
|
85
|
+
v2 https://support.huaweicloud.com/api-dns/ListRecordSetsByZone.html
|
|
86
|
+
"""
|
|
87
|
+
domain = join_domain(subdomain, main_domain) + "."
|
|
88
|
+
data = self._request(
|
|
89
|
+
"GET",
|
|
90
|
+
"/v2.1/zones/" + zone_id + "/recordsets",
|
|
91
|
+
limit=500,
|
|
92
|
+
name=domain,
|
|
93
|
+
type=record_type,
|
|
94
|
+
line_id=line,
|
|
95
|
+
search_mode="equal",
|
|
96
|
+
)
|
|
97
|
+
records = data.get("recordsets", [])
|
|
98
|
+
record = next((r for r in records if r.get("name") == domain and r.get("type") == record_type), None)
|
|
99
|
+
return record
|
|
100
|
+
|
|
101
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
102
|
+
"""
|
|
103
|
+
v2.1 https://support.huaweicloud.com/api-dns/dns_api_64001.html
|
|
104
|
+
v2 https://support.huaweicloud.com/api-dns/CreateRecordSet.html
|
|
105
|
+
"""
|
|
106
|
+
domain = join_domain(subdomain, main_domain) + "."
|
|
107
|
+
extra["description"] = extra.get("description", self.remark)
|
|
108
|
+
res = self._request(
|
|
109
|
+
"POST",
|
|
110
|
+
"/v2.1/zones/" + zone_id + "/recordsets",
|
|
111
|
+
name=domain,
|
|
112
|
+
type=record_type,
|
|
113
|
+
records=[value],
|
|
114
|
+
ttl=ttl,
|
|
115
|
+
line=line,
|
|
116
|
+
**extra
|
|
117
|
+
)
|
|
118
|
+
if res and res.get("id"):
|
|
119
|
+
self.logger.info("Record created: %s", res)
|
|
120
|
+
return True
|
|
121
|
+
self.logger.warning("Failed to create record: %s", res)
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
125
|
+
"""https://support.huaweicloud.com/api-dns/UpdateRecordSets.html"""
|
|
126
|
+
extra["description"] = extra.get("description", self.remark)
|
|
127
|
+
# Note: The v2.1 update API does not support the line parameter in the request body
|
|
128
|
+
# The line parameter is returned in the response but cannot be modified
|
|
129
|
+
res = self._request(
|
|
130
|
+
"PUT",
|
|
131
|
+
"/v2.1/zones/" + zone_id + "/recordsets/" + old_record["id"],
|
|
132
|
+
name=old_record["name"],
|
|
133
|
+
type=record_type,
|
|
134
|
+
records=[value],
|
|
135
|
+
ttl=ttl if ttl is not None else old_record.get("ttl"),
|
|
136
|
+
**extra
|
|
137
|
+
)
|
|
138
|
+
if res and res.get("id"):
|
|
139
|
+
self.logger.info("Record updated: %s", res)
|
|
140
|
+
return True
|
|
141
|
+
self.logger.warning("Failed to update record: %s", res)
|
|
142
|
+
return False
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
"""
|
|
3
|
+
NameSilo API Provider
|
|
4
|
+
DNS provider implementation for NameSilo domain registrar
|
|
5
|
+
@doc: https://www.namesilo.com/api-reference
|
|
6
|
+
@author: NewFuture & Copilot
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._base import BaseProvider, TYPE_JSON
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NamesiloProvider(BaseProvider):
|
|
13
|
+
"""
|
|
14
|
+
NameSilo DNS API Provider
|
|
15
|
+
|
|
16
|
+
Supports DNS record management through NameSilo's API including:
|
|
17
|
+
- Domain information retrieval
|
|
18
|
+
- DNS record listing
|
|
19
|
+
- DNS record creation
|
|
20
|
+
- DNS record updating
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
endpoint = "https://www.namesilo.com"
|
|
24
|
+
content_type = TYPE_JSON
|
|
25
|
+
|
|
26
|
+
def _validate(self):
|
|
27
|
+
"""Validate authentication credentials"""
|
|
28
|
+
# NameSilo only requires API key (token), not ID
|
|
29
|
+
if not self.token:
|
|
30
|
+
raise ValueError("API key (token) must be configured for NameSilo")
|
|
31
|
+
if not self.endpoint:
|
|
32
|
+
raise ValueError("API endpoint must be defined in {}".format(self.__class__.__name__))
|
|
33
|
+
|
|
34
|
+
# Warn if ID is configured since NameSilo doesn't need it
|
|
35
|
+
if self.id:
|
|
36
|
+
self.logger.warning("NameSilo does not require 'id' configuration - only API key (token) is needed")
|
|
37
|
+
|
|
38
|
+
# Show pending verification warning
|
|
39
|
+
self.logger.warning("NameSilo provider implementation is pending verification - please test thoroughly")
|
|
40
|
+
|
|
41
|
+
def _request(self, operation, **params):
|
|
42
|
+
# type: (str, **(str | int | bytes | bool | None)) -> dict|None
|
|
43
|
+
"""
|
|
44
|
+
Send request to NameSilo API
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
operation (str): API operation name
|
|
48
|
+
params: API parameters
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
dict: API response data
|
|
52
|
+
"""
|
|
53
|
+
# Filter out None parameters
|
|
54
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
55
|
+
|
|
56
|
+
# Add required authentication and format parameters
|
|
57
|
+
params.update({"version": "1", "type": "json", "key": self.token})
|
|
58
|
+
|
|
59
|
+
# Make API request
|
|
60
|
+
response = self._http("GET", "/api/" + operation, queries=params)
|
|
61
|
+
|
|
62
|
+
# Parse response
|
|
63
|
+
if response and isinstance(response, dict):
|
|
64
|
+
reply = response.get("reply", {})
|
|
65
|
+
|
|
66
|
+
# Check for successful response
|
|
67
|
+
if reply.get("code") == "300": # NameSilo success code
|
|
68
|
+
return reply
|
|
69
|
+
else:
|
|
70
|
+
# Log error details
|
|
71
|
+
error_msg = reply.get("detail", "Unknown error")
|
|
72
|
+
self.logger.warning("NameSilo API error [%s]: %s", reply.get("code", "unknown"), error_msg)
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def _query_zone_id(self, domain):
|
|
77
|
+
# type: (str) -> str | None
|
|
78
|
+
"""
|
|
79
|
+
Query domain information to get domain as zone identifier
|
|
80
|
+
@doc: https://www.namesilo.com/api-reference#domains/get-domain-info
|
|
81
|
+
"""
|
|
82
|
+
response = self._request("getDomainInfo", domain=domain)
|
|
83
|
+
|
|
84
|
+
if response:
|
|
85
|
+
# Domain exists, return the domain name as zone_id
|
|
86
|
+
domain_info = response.get("domain", {})
|
|
87
|
+
if domain_info:
|
|
88
|
+
self.logger.debug("Domain found: %s", domain)
|
|
89
|
+
return domain
|
|
90
|
+
|
|
91
|
+
self.logger.warning("Domain not found or not accessible: %s", domain)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
95
|
+
# type: (str, str, str, str, str | None, dict) -> dict | None
|
|
96
|
+
"""
|
|
97
|
+
Query existing DNS record
|
|
98
|
+
@doc: https://www.namesilo.com/api-reference#dns/list-dns-records
|
|
99
|
+
"""
|
|
100
|
+
response = self._request("dnsListRecords", domain=main_domain)
|
|
101
|
+
|
|
102
|
+
if response:
|
|
103
|
+
records = response.get("resource_record", [])
|
|
104
|
+
|
|
105
|
+
# Find matching record
|
|
106
|
+
for record in records:
|
|
107
|
+
if record.get("host") == subdomain and record.get("type") == record_type:
|
|
108
|
+
self.logger.debug("Found existing record: %s", record)
|
|
109
|
+
return record
|
|
110
|
+
|
|
111
|
+
self.logger.debug("No matching record found for %s.%s (%s)", subdomain, main_domain, record_type)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
115
|
+
# type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
|
|
116
|
+
"""
|
|
117
|
+
Create new DNS record
|
|
118
|
+
@doc: https://www.namesilo.com/api-reference#dns/add-dns-record
|
|
119
|
+
"""
|
|
120
|
+
response = self._request(
|
|
121
|
+
"dnsAddRecord", domain=main_domain, rrtype=record_type, rrhost=subdomain, rrvalue=value, rrttl=ttl
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if response:
|
|
125
|
+
record_id = response.get("record_id")
|
|
126
|
+
self.logger.info("DNS record created successfully: %s", record_id)
|
|
127
|
+
return True
|
|
128
|
+
else:
|
|
129
|
+
self.logger.error("Failed to create DNS record")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
133
|
+
# type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
|
|
134
|
+
"""
|
|
135
|
+
Update existing DNS record
|
|
136
|
+
@doc: https://www.namesilo.com/api-reference#dns/update-dns-record
|
|
137
|
+
"""
|
|
138
|
+
record_id = old_record.get("record_id")
|
|
139
|
+
if not record_id:
|
|
140
|
+
self.logger.error("No record_id found in old_record: %s", old_record)
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
# In NameSilo, zone_id is the main domain name
|
|
144
|
+
response = self._request(
|
|
145
|
+
"dnsUpdateRecord",
|
|
146
|
+
rrid=record_id,
|
|
147
|
+
domain=zone_id, # zone_id is main_domain in NameSilo
|
|
148
|
+
rrhost=old_record.get("host"), # host field contains subdomain
|
|
149
|
+
rrvalue=value,
|
|
150
|
+
rrtype=record_type,
|
|
151
|
+
rrttl=ttl or old_record.get("ttl"),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if response:
|
|
155
|
+
self.logger.info("DNS record updated successfully: %s", record_id)
|
|
156
|
+
return True
|
|
157
|
+
else:
|
|
158
|
+
self.logger.error("Failed to update DNS record")
|
|
159
|
+
return False
|
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
|