ddns 4.0.1__py2.py3-none-any.whl → 4.1.0b1__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 +5 -0
- ddns/__init__.py +6 -4
- ddns/__main__.py +75 -69
- ddns/provider/__init__.py +59 -0
- ddns/provider/_base.py +659 -0
- ddns/provider/alidns.py +127 -176
- ddns/provider/callback.py +67 -105
- ddns/provider/cloudflare.py +87 -151
- ddns/provider/debug.py +20 -0
- ddns/provider/dnscom.py +91 -169
- ddns/provider/dnspod.py +105 -174
- ddns/provider/dnspod_com.py +11 -8
- ddns/provider/he.py +41 -78
- ddns/provider/huaweidns.py +133 -256
- ddns/provider/tencentcloud.py +195 -0
- ddns/util/cache.py +67 -54
- ddns/util/config.py +29 -32
- ddns/util/http.py +277 -0
- ddns/util/ip.py +20 -17
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/METADATA +96 -71
- ddns-4.1.0b1.dist-info/RECORD +26 -0
- ddns-4.0.1.dist-info/RECORD +0 -21
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/WHEEL +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/entry_points.txt +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/licenses/LICENSE +0 -0
- {ddns-4.0.1.dist-info → ddns-4.1.0b1.dist-info}/top_level.txt +0 -0
ddns/provider/alidns.py
CHANGED
|
@@ -2,182 +2,133 @@
|
|
|
2
2
|
"""
|
|
3
3
|
AliDNS API
|
|
4
4
|
阿里DNS解析操作库
|
|
5
|
-
|
|
6
|
-
@author: New Future
|
|
5
|
+
@author: NewFuture
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if response.status < 200 or response.status >= 300:
|
|
89
|
-
warning('%s : error[%d]: %s', params['Action'], response.status, data)
|
|
90
|
-
raise Exception(data)
|
|
91
|
-
else:
|
|
92
|
-
data = jsondecode(data)
|
|
93
|
-
debug('%s : result:%s', params['Action'], data)
|
|
94
|
-
return data
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def get_domain_info(domain):
|
|
98
|
-
"""
|
|
99
|
-
切割域名获取主域名和对应ID
|
|
100
|
-
https://help.aliyun.com/document_detail/29755.html
|
|
101
|
-
http://alidns.aliyuncs.com/?Action=GetMainDomainName&InputString=www.example.com
|
|
102
|
-
"""
|
|
103
|
-
res = request(Action="GetMainDomainName", InputString=domain)
|
|
104
|
-
sub, main = res.get('RR'), res.get('DomainName')
|
|
105
|
-
return sub, main
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def get_records(domain, **conditions):
|
|
109
|
-
"""
|
|
110
|
-
获取记录ID
|
|
111
|
-
返回满足条件的所有记录[]
|
|
112
|
-
https://help.aliyun.com/document_detail/29776.html
|
|
113
|
-
TODO 大于500翻页
|
|
114
|
-
"""
|
|
115
|
-
if not hasattr(get_records, "records"):
|
|
116
|
-
get_records.records = {} # "静态变量"存储已查询过的id
|
|
117
|
-
get_records.keys = ("RecordId", "RR", "Type", "Line",
|
|
118
|
-
"Locked", "Status", "Priority", "Value")
|
|
119
|
-
|
|
120
|
-
if domain not in get_records.records:
|
|
121
|
-
get_records.records[domain] = {}
|
|
122
|
-
data = request(Action="DescribeDomainRecords",
|
|
123
|
-
DomainName=domain, PageSize=500)
|
|
124
|
-
if data:
|
|
125
|
-
for record in data.get('DomainRecords').get('Record'):
|
|
126
|
-
get_records.records[domain][record["RecordId"]] = {
|
|
127
|
-
k: v for (k, v) in record.items() if k in get_records.keys}
|
|
128
|
-
records = {}
|
|
129
|
-
for (rid, record) in get_records.records[domain].items():
|
|
130
|
-
for (k, value) in conditions.items():
|
|
131
|
-
if record.get(k) != value:
|
|
132
|
-
break
|
|
133
|
-
else: # for else push
|
|
134
|
-
records[rid] = record
|
|
135
|
-
return records
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def update_record(domain, value, record_type='A'):
|
|
139
|
-
"""
|
|
140
|
-
更新记录
|
|
141
|
-
update
|
|
142
|
-
https://help.aliyun.com/document_detail/29774.html
|
|
143
|
-
add
|
|
144
|
-
https://help.aliyun.com/document_detail/29772.html?
|
|
145
|
-
"""
|
|
146
|
-
debug(">>>>>%s(%s)", domain, record_type)
|
|
147
|
-
sub, main = get_domain_info(domain)
|
|
148
|
-
if not sub:
|
|
149
|
-
raise Exception("invalid domain: [ %s ] " % domain)
|
|
150
|
-
|
|
151
|
-
records = get_records(main, RR=sub, Type=record_type)
|
|
152
|
-
result = {}
|
|
153
|
-
|
|
154
|
-
if records:
|
|
155
|
-
for (rid, record) in records.items():
|
|
156
|
-
if record["Value"] != value:
|
|
157
|
-
debug(sub, record)
|
|
158
|
-
res = request(Action="UpdateDomainRecord", RecordId=rid,
|
|
159
|
-
Value=value, RR=sub, Type=record_type, TTL=Config.TTL)
|
|
160
|
-
if res:
|
|
161
|
-
# update records
|
|
162
|
-
get_records.records[main][rid]["Value"] = value
|
|
163
|
-
result[rid] = res
|
|
164
|
-
else:
|
|
165
|
-
result[rid] = "update fail!\n" + str(res)
|
|
166
|
-
else:
|
|
167
|
-
result[rid] = domain
|
|
168
|
-
else: # https://help.aliyun.com/document_detail/29772.html
|
|
169
|
-
res = request(Action="AddDomainRecord", DomainName=main,
|
|
170
|
-
Value=value, RR=sub, Type=record_type, TTL=Config.TTL)
|
|
171
|
-
if res:
|
|
172
|
-
# update records INFO
|
|
173
|
-
rid = res.get('RecordId')
|
|
174
|
-
get_records.records[main][rid] = {
|
|
175
|
-
'Value': value,
|
|
176
|
-
"RecordId": rid,
|
|
177
|
-
"RR": sub,
|
|
178
|
-
"Type": record_type
|
|
179
|
-
}
|
|
180
|
-
result = res
|
|
8
|
+
from ._base import TYPE_FORM, BaseProvider, hmac_sha256_authorization, sha256_hash, join_domain
|
|
9
|
+
from time import strftime, gmtime, time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AlidnsProvider(BaseProvider):
|
|
13
|
+
API = "https://alidns.aliyuncs.com"
|
|
14
|
+
content_type = TYPE_FORM # 阿里云DNS API使用表单格式
|
|
15
|
+
|
|
16
|
+
api_version = "2015-01-09" # API版本,v3签名需要
|
|
17
|
+
|
|
18
|
+
def _request(self, action, **params):
|
|
19
|
+
# type: (str, **(str | int | bytes | bool | None)) -> dict
|
|
20
|
+
"""Aliyun v3 https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature"""
|
|
21
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
22
|
+
body_content = self._encode(params) if len(params) > 0 else ""
|
|
23
|
+
content_hash = sha256_hash(body_content)
|
|
24
|
+
# 构造请求头部
|
|
25
|
+
headers = {
|
|
26
|
+
"host": self.API.split("://", 1)[1].strip("/"),
|
|
27
|
+
"content-type": self.content_type,
|
|
28
|
+
"x-acs-action": action,
|
|
29
|
+
"x-acs-content-sha256": content_hash,
|
|
30
|
+
"x-acs-date": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()),
|
|
31
|
+
"x-acs-signature-nonce": str(hash(time()))[2:],
|
|
32
|
+
"x-acs-version": self.api_version,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# 使用通用签名函数
|
|
36
|
+
authorization = hmac_sha256_authorization(
|
|
37
|
+
secret_key=self.auth_token,
|
|
38
|
+
method="POST",
|
|
39
|
+
path="/",
|
|
40
|
+
query="",
|
|
41
|
+
headers=headers,
|
|
42
|
+
body_hash=content_hash,
|
|
43
|
+
signing_string_format="ACS3-HMAC-SHA256\n{HashedCanonicalRequest}",
|
|
44
|
+
authorization_format=(
|
|
45
|
+
"ACS3-HMAC-SHA256 Credential=" + self.auth_id + ",SignedHeaders={SignedHeaders},Signature={Signature}"
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
headers["Authorization"] = authorization
|
|
49
|
+
# 对于v3签名的RPC API,参数在request body中
|
|
50
|
+
return self._http("POST", "/", body=body_content, headers=headers)
|
|
51
|
+
|
|
52
|
+
def _split_zone_and_sub(self, domain):
|
|
53
|
+
# type: (str) -> tuple[str | None, str | None, str]
|
|
54
|
+
"""
|
|
55
|
+
AliDNS 支持直接查询主域名和RR,无需循环查询。
|
|
56
|
+
返回没有DomainId,用DomainName代替
|
|
57
|
+
https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-getmaindomainname
|
|
58
|
+
"""
|
|
59
|
+
res = self._request("GetMainDomainName", InputString=domain)
|
|
60
|
+
sub, main = res.get("RR"), res.get("DomainName")
|
|
61
|
+
return (main, sub, main or domain)
|
|
62
|
+
|
|
63
|
+
def _query_zone_id(self, domain):
|
|
64
|
+
"""调用_split_zone_and_sub可直接获取,无需调用_query_zone_id"""
|
|
65
|
+
raise NotImplementedError("_split_zone_and_sub is used to get zone_id")
|
|
66
|
+
|
|
67
|
+
def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
|
|
68
|
+
"""https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-describesubdomainrecords"""
|
|
69
|
+
sub = join_domain(subdomain, main_domain)
|
|
70
|
+
data = self._request(
|
|
71
|
+
"DescribeSubDomainRecords",
|
|
72
|
+
SubDomain=sub, # aliyun API要求SubDomain为完整域名
|
|
73
|
+
DomainName=main_domain,
|
|
74
|
+
Type=record_type,
|
|
75
|
+
Line=line,
|
|
76
|
+
PageSize=500,
|
|
77
|
+
Lang=extra.get("Lang"), # 默认中文
|
|
78
|
+
Status=extra.get("Status"), # 默认全部状态
|
|
79
|
+
)
|
|
80
|
+
records = data.get("DomainRecords", {}).get("Record", [])
|
|
81
|
+
if not records:
|
|
82
|
+
self.logger.warning(
|
|
83
|
+
"No records found for [%s] with %s <%s> (line: %s)", zone_id, subdomain, record_type, line
|
|
84
|
+
)
|
|
85
|
+
elif not isinstance(records, list):
|
|
86
|
+
self.logger.error("Invalid records format: %s", records)
|
|
181
87
|
else:
|
|
182
|
-
|
|
183
|
-
|
|
88
|
+
return next((r for r in records), None)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
|
|
92
|
+
"""https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-adddomainrecord"""
|
|
93
|
+
data = self._request(
|
|
94
|
+
"AddDomainRecord",
|
|
95
|
+
DomainName=main_domain,
|
|
96
|
+
RR=subdomain,
|
|
97
|
+
Value=value,
|
|
98
|
+
Type=record_type,
|
|
99
|
+
TTL=ttl,
|
|
100
|
+
Line=line,
|
|
101
|
+
**extra
|
|
102
|
+
)
|
|
103
|
+
if data and data.get("RecordId"):
|
|
104
|
+
self.logger.info("Record created: %s", data)
|
|
105
|
+
return True
|
|
106
|
+
self.logger.error("Failed to create record: %s", data)
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
|
|
110
|
+
"""https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-updatedomainrecord"""
|
|
111
|
+
# 阿里云DNS update新旧值不能一样,先判断是否发生变化
|
|
112
|
+
if (
|
|
113
|
+
old_record.get("Value") == value
|
|
114
|
+
and old_record.get("Type") == record_type
|
|
115
|
+
and (not ttl or old_record.get("TTL") == ttl)
|
|
116
|
+
):
|
|
117
|
+
domain = join_domain(old_record.get("RR"), old_record.get("DomainName"))
|
|
118
|
+
self.logger.warning("No changes detected, skipping update for record: %s", domain)
|
|
119
|
+
return True
|
|
120
|
+
data = self._request(
|
|
121
|
+
"UpdateDomainRecord",
|
|
122
|
+
RecordId=old_record.get("RecordId"),
|
|
123
|
+
Value=value,
|
|
124
|
+
RR=old_record.get("RR"),
|
|
125
|
+
Type=record_type,
|
|
126
|
+
TTL=ttl,
|
|
127
|
+
Line=line or old_record.get("Line"),
|
|
128
|
+
**extra
|
|
129
|
+
)
|
|
130
|
+
if data and data.get("RecordId"):
|
|
131
|
+
self.logger.info("Record updated: %s", data)
|
|
132
|
+
return True
|
|
133
|
+
self.logger.error("Failed to update record: %s", data)
|
|
134
|
+
return False
|
ddns/provider/callback.py
CHANGED
|
@@ -3,115 +3,77 @@
|
|
|
3
3
|
Custom Callback API
|
|
4
4
|
自定义回调接口解析操作库
|
|
5
5
|
|
|
6
|
-
@author:
|
|
6
|
+
@author: 老周部落, NewFuture
|
|
7
7
|
"""
|
|
8
|
-
|
|
9
|
-
from json import loads as jsondecode
|
|
10
|
-
from logging import debug, info, warning
|
|
8
|
+
from ._base import TYPE_JSON, SimpleProvider
|
|
11
9
|
from time import time
|
|
12
|
-
|
|
13
|
-
try: # python 3
|
|
14
|
-
from http.client import HTTPSConnection, HTTPConnection
|
|
15
|
-
from urllib.parse import urlencode, urlparse, parse_qsl
|
|
16
|
-
except ImportError: # python 2
|
|
17
|
-
from httplib import HTTPSConnection, HTTPConnection
|
|
18
|
-
from urlparse import urlparse, parse_qsl
|
|
19
|
-
from urllib import urlencode
|
|
20
|
-
|
|
21
|
-
__author__ = '老周部落'
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class Config:
|
|
25
|
-
ID = None # 自定义回调 URL
|
|
26
|
-
TOKEN = None # 使用 JSON 编码的 POST 参数
|
|
27
|
-
PROXY = None # 代理设置
|
|
28
|
-
TTL = None
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def request(method, action, param=None, **params):
|
|
32
|
-
"""
|
|
33
|
-
发送请求数据
|
|
34
|
-
"""
|
|
35
|
-
if param:
|
|
36
|
-
params.update(param)
|
|
37
|
-
|
|
38
|
-
URLObj = urlparse(Config.ID)
|
|
39
|
-
params = dict((k, params[k]) for k in params if params[k] is not None)
|
|
40
|
-
info("%s/%s : %s", URLObj.netloc, action, params)
|
|
41
|
-
|
|
42
|
-
if Config.PROXY:
|
|
43
|
-
if URLObj.netloc == "http":
|
|
44
|
-
conn = HTTPConnection(Config.PROXY)
|
|
45
|
-
else:
|
|
46
|
-
conn = HTTPSConnection(Config.PROXY)
|
|
47
|
-
conn.set_tunnel(URLObj.netloc, URLObj.port)
|
|
48
|
-
else:
|
|
49
|
-
if URLObj.netloc == "http":
|
|
50
|
-
conn = HTTPConnection(URLObj.netloc, URLObj.port)
|
|
51
|
-
else:
|
|
52
|
-
conn = HTTPSConnection(URLObj.netloc, URLObj.port)
|
|
53
|
-
|
|
54
|
-
headers = {}
|
|
55
|
-
|
|
56
|
-
if method == "GET":
|
|
57
|
-
if params:
|
|
58
|
-
action += '?' + urlencode(params)
|
|
59
|
-
params = ""
|
|
60
|
-
else:
|
|
61
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
62
|
-
|
|
63
|
-
params = urlencode(params)
|
|
64
|
-
|
|
65
|
-
conn.request(method, action, params, headers)
|
|
66
|
-
response = conn.getresponse()
|
|
67
|
-
res = response.read().decode('utf8')
|
|
68
|
-
conn.close()
|
|
69
|
-
if response.status < 200 or response.status >= 300:
|
|
70
|
-
warning('%s : error[%d]:%s', action, response.status, res)
|
|
71
|
-
raise Exception(res)
|
|
72
|
-
else:
|
|
73
|
-
debug('%s : result:%s', action, res)
|
|
74
|
-
return res
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def replace_params(domain, record_type, ip, params):
|
|
78
|
-
"""
|
|
79
|
-
替换定义常量为实际值
|
|
80
|
-
"""
|
|
81
|
-
dict = {"__DOMAIN__": domain, "__RECORDTYPE__": record_type,
|
|
82
|
-
"__TTL__": Config.TTL, "__TIMESTAMP__": time(), "__IP__": ip}
|
|
83
|
-
for key, value in params.items():
|
|
84
|
-
if dict.get(value):
|
|
85
|
-
params[key] = dict.get(value)
|
|
86
|
-
return params
|
|
10
|
+
from json import loads as jsondecode
|
|
87
11
|
|
|
88
12
|
|
|
89
|
-
|
|
13
|
+
class CallbackProvider(SimpleProvider):
|
|
90
14
|
"""
|
|
91
|
-
|
|
15
|
+
通用自定义回调 Provider,支持 GET/POST 任意接口。
|
|
16
|
+
Generic custom callback provider, supports GET/POST arbitrary API.
|
|
92
17
|
"""
|
|
93
|
-
info(">>>>>%s(%s)", domain, record_type)
|
|
94
|
-
|
|
95
|
-
result = {}
|
|
96
|
-
|
|
97
|
-
if not Config.TOKEN: # 此处使用 TOKEN 参数透传 POST 参数所用的 JSON
|
|
98
|
-
method = "GET"
|
|
99
|
-
URLObj = urlparse(Config.ID)
|
|
100
|
-
path = URLObj.path
|
|
101
|
-
query = dict(parse_qsl(URLObj.query))
|
|
102
|
-
params = replace_params(domain, record_type, value, query)
|
|
103
|
-
else:
|
|
104
|
-
method = "POST"
|
|
105
|
-
URLObj = urlparse(Config.ID)
|
|
106
|
-
path = URLObj.path
|
|
107
|
-
params = replace_params(domain, record_type,
|
|
108
|
-
value, jsondecode(Config.TOKEN))
|
|
109
|
-
|
|
110
|
-
res = request(method, path, params)
|
|
111
|
-
|
|
112
|
-
if res:
|
|
113
|
-
result = "Callback Request Success!\n" + res
|
|
114
|
-
else:
|
|
115
|
-
result = "Callback Request Fail!\n"
|
|
116
18
|
|
|
117
|
-
|
|
19
|
+
API = "" # CallbackProvider uses auth_id as URL, no fixed API endpoint
|
|
20
|
+
content_type = TYPE_JSON
|
|
21
|
+
decode_response = False # Callback response is not JSON, it's a custom response
|
|
22
|
+
|
|
23
|
+
def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
|
|
24
|
+
"""
|
|
25
|
+
发送自定义回调请求,支持 GET/POST
|
|
26
|
+
Send custom callback request, support GET/POST
|
|
27
|
+
"""
|
|
28
|
+
self.logger.info("%s => %s(%s)", domain, value, record_type)
|
|
29
|
+
url = self.auth_id # 直接用 auth_id 作为 url
|
|
30
|
+
token = self.auth_token # auth_token 作为 POST 参数
|
|
31
|
+
headers = {"User-Agent": "DDNS/{0} (ddns@newfuture.cc)".format(self.version)}
|
|
32
|
+
extra.update(
|
|
33
|
+
{
|
|
34
|
+
"__DOMAIN__": domain,
|
|
35
|
+
"__RECORDTYPE__": record_type,
|
|
36
|
+
"__TTL__": ttl,
|
|
37
|
+
"__IP__": value,
|
|
38
|
+
"__TIMESTAMP__": time(),
|
|
39
|
+
"__LINE__": line,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
url = self._replace_vars(url, extra)
|
|
43
|
+
method, params = "GET", None
|
|
44
|
+
if token:
|
|
45
|
+
# 如果有 token,使用 POST 方法
|
|
46
|
+
method = "POST"
|
|
47
|
+
# POST 方式,token 作为 POST 参数
|
|
48
|
+
params = token if isinstance(token, dict) else jsondecode(token)
|
|
49
|
+
for k, v in params.items():
|
|
50
|
+
if hasattr(v, "replace"): # 判断是否支持字符串替换, 兼容py2,py3
|
|
51
|
+
params[k] = self._replace_vars(v, extra)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
res = self._http(method, url, body=params, headers=headers)
|
|
55
|
+
if res is not None:
|
|
56
|
+
self.logger.info("Callback result: %s", res)
|
|
57
|
+
return True
|
|
58
|
+
else:
|
|
59
|
+
self.logger.warning("Callback received empty response.")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
self.logger.error("Callback failed: %s", e)
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def _replace_vars(self, string, mapping):
|
|
65
|
+
# type: (str, dict) -> str
|
|
66
|
+
"""
|
|
67
|
+
替换字符串中的变量为实际值
|
|
68
|
+
Replace variables in string with actual values
|
|
69
|
+
"""
|
|
70
|
+
for k, v in mapping.items():
|
|
71
|
+
string = string.replace(k, str(v))
|
|
72
|
+
return string
|
|
73
|
+
|
|
74
|
+
def _validate(self):
|
|
75
|
+
# CallbackProvider uses auth_id as URL, not as regular ID
|
|
76
|
+
if not self.auth_id or "://" not in self.auth_id:
|
|
77
|
+
self.logger.critical("callback ID 参数[%s] 必须是有效的URL", self.auth_id)
|
|
78
|
+
raise ValueError("id must be configured with URL")
|
|
79
|
+
# CallbackProvider doesn't need auth_token validation (it can be empty)
|