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.

@@ -1,163 +1,99 @@
1
1
  # coding=utf-8
2
2
  """
3
3
  CloudFlare API
4
- CloudFlare 接口解析操作库
5
- https://api.cloudflare.com/#dns-records-for-a-zone-properties
6
- @author: TongYifan
4
+ @author: TongYifan, NewFuture
7
5
  """
8
6
 
9
- from json import loads as jsondecode, dumps as jsonencode
10
- from logging import debug, info, warning
7
+ from ._base import BaseProvider, TYPE_JSON, join_domain
11
8
 
12
- try: # python 3
13
- from http.client import HTTPSConnection
14
- from urllib.parse import urlencode
15
- except ImportError: # python 2
16
- from httplib import HTTPSConnection
17
- from urllib import urlencode
18
9
 
19
- __author__ = 'TongYifan'
10
+ class CloudflareProvider(BaseProvider):
11
+ API = "https://api.cloudflare.com"
12
+ content_type = TYPE_JSON
20
13
 
14
+ def _validate(self):
15
+ if not self.auth_token:
16
+ raise ValueError("token must be configured")
17
+ if self.auth_id:
18
+ # must be email for Cloudflare API v4
19
+ if "@" not in self.auth_id:
20
+ self.logger.critical("ID 必须为空或有效的邮箱地址")
21
+ raise ValueError("ID must be a valid email or Empty for Cloudflare API v4")
21
22
 
22
- class Config:
23
- ID = "AUTH EMAIL" # CloudFlare 验证的是用户Email,等同于其他平台的userID
24
- TOKEN = "API KEY"
25
- PROXY = None # 代理设置
26
- TTL = None
27
-
28
-
29
- class API:
30
- # API 配置
31
- SITE = "api.cloudflare.com" # API endpoint
32
-
33
-
34
- def request(method, action, param=None, **params):
35
- """
36
- 发送请求数据
37
- """
38
- if param:
39
- params.update(param)
40
-
41
- params = dict((k, params[k]) for k in params if params[k] is not None)
42
- info("%s/%s : %s", API.SITE, action, params)
43
- if Config.PROXY:
44
- conn = HTTPSConnection(Config.PROXY)
45
- conn.set_tunnel(API.SITE, 443)
46
- else:
47
- conn = HTTPSConnection(API.SITE)
48
-
49
- if method in ['PUT', 'POST', 'PATCH']:
50
- # 从public_v(4,6)获取的IP是bytes类型,在json.dumps时会报TypeError
51
- params['content'] = str(params.get('content'))
52
- params = jsonencode(params)
53
- else: # (GET, DELETE) where DELETE doesn't require params in Cloudflare
54
- if params:
55
- action += '?' + urlencode(params)
56
- params = None
57
- if not Config.ID:
58
- headers = {"Content-type": "application/json",
59
- "Authorization": "Bearer " + Config.TOKEN}
60
- else:
61
- headers = {"Content-type": "application/json",
62
- "X-Auth-Email": Config.ID, "X-Auth-Key": Config.TOKEN}
63
- conn.request(method, '/client/v4/zones' + action, params, headers)
64
- response = conn.getresponse()
65
- res = response.read().decode('utf8')
66
- conn.close()
67
- if response.status < 200 or response.status >= 300:
68
- warning('%s : error[%d]:%s', action, response.status, res)
69
- raise Exception(res)
70
- else:
71
- data = jsondecode(res)
72
- debug('%s : result:%s', action, data)
73
- if not data:
74
- raise Exception("Empty Response")
75
- elif data.get('success'):
76
- return data.get('result', [{}])
23
+ def _request(self, method, action, **params):
24
+ """发送请求数据"""
25
+ headers = {}
26
+ if self.auth_id:
27
+ headers["X-Auth-Email"] = self.auth_id
28
+ headers["X-Auth-Key"] = self.auth_token
77
29
  else:
78
- raise Exception(data.get('errors', [{}]))
79
-
80
-
81
- def get_zone_id(domain):
82
- """
83
- 切割域名获取主域名ID(Zone_ID)
84
- https://api.cloudflare.com/#zone-list-zones
85
- """
86
- zoneid = None
87
- domain_slice = domain.split('.')
88
- index = 2
89
- # ddns.example.com => example.com; ddns.example.eu.org => example.eu.org
90
- while (not zoneid) and (index <= len(domain_slice)):
91
- zones = request('GET', '', name='.'.join(domain_slice[-index:]))
92
- zone = next((z for z in zones if domain.endswith(z.get('name'))), None)
93
- zoneid = zone and zone['id']
94
- index += 1
95
- return zoneid
96
-
30
+ headers["Authorization"] = "Bearer " + self.auth_token
97
31
 
98
- def get_records(zoneid, **conditions):
99
- """
100
- 获取记录ID
101
- 返回满足条件的所有记录[]
102
- TODO 大于100翻页
103
- """
104
- cache_key = zoneid + "_" + \
105
- conditions.get('name', "") + "_" + conditions.get('type', "")
106
- if not hasattr(get_records, 'records'):
107
- get_records.records = {} # "静态变量"存储已查询过的id
108
- get_records.keys = ('id', 'type', 'name', 'content', 'proxied', 'ttl')
109
-
110
- if zoneid not in get_records.records:
111
- get_records.records[cache_key] = {}
112
- data = request('GET', '/' + zoneid + '/dns_records',
113
- per_page=100, **conditions)
114
- if data:
115
- for record in data:
116
- get_records.records[cache_key][record['id']] = {
117
- k: v for (k, v) in record.items() if k in get_records.keys}
118
-
119
- records = {}
120
- for (zid, record) in get_records.records[cache_key].items():
121
- for (k, value) in conditions.items():
122
- if record.get(k) != value:
123
- break
124
- else: # for else push
125
- records[zid] = record
126
- return records
127
-
128
-
129
- def update_record(domain, value, record_type="A"):
130
- """
131
- 更新记录
132
- """
133
- info(">>>>>%s(%s)", domain, record_type)
134
- zoneid = get_zone_id(domain)
135
- if not zoneid:
136
- raise Exception("invalid domain: [ %s ] " % domain)
137
-
138
- records = get_records(zoneid, name=domain, type=record_type)
139
- cache_key = zoneid + "_" + domain + "_" + record_type
140
- result = {}
141
- if records: # update
142
- # https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
143
- for (rid, record) in records.items():
144
- if record['content'] != value:
145
- res = request('PUT', '/' + zoneid + '/dns_records/' + record['id'],
146
- type=record_type, content=value, name=domain, proxied=record['proxied'], ttl=Config.TTL)
147
- if res:
148
- get_records.records[cache_key][rid]['content'] = value
149
- result[rid] = res.get("name")
150
- else:
151
- result[rid] = "Update fail!\n" + str(res)
152
- else:
153
- result[rid] = domain
154
- else: # create
155
- # https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
156
- res = request('POST', '/' + zoneid + '/dns_records',
157
- type=record_type, name=domain, content=value, proxied=False, ttl=Config.TTL)
158
- if res:
159
- get_records.records[cache_key][res['id']] = res
160
- result = res
32
+ params = {k: v for k, v in params.items() if v is not None} # 过滤掉None参数
33
+ data = self._http(method, "/client/v4/zones" + action, headers=headers, params=params)
34
+ if data and data.get("success"):
35
+ return data.get("result") # 返回结果或原始数据
161
36
  else:
162
- result = domain + " created fail!"
163
- return result
37
+ self.logger.warning("Cloudflare API error: %s", data.get("errors", "Unknown error"))
38
+ return data
39
+
40
+ def _query_zone_id(self, domain):
41
+ """https://developers.cloudflare.com/api/resources/zones/methods/list/"""
42
+ params = {"name.exact": domain, "per_page": 50}
43
+ zones = self._request("GET", "", **params)
44
+ zone = next((z for z in zones if domain == z.get("name", "")), None)
45
+ self.logger.debug("Queried zone: %s", zone)
46
+ if zone:
47
+ return zone["id"]
48
+ return None
49
+
50
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
51
+ # type: (str, str, str, str, str | None, dict) -> dict | None
52
+ """https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/list/"""
53
+ # cloudflare的域名查询需要完整域名
54
+ name = join_domain(subdomain, main_domain)
55
+ query = {"name.exact": name} # type: dict[str, str|None]
56
+ if extra:
57
+ query["proxied"] = extra.get("proxied", None) # 代理状态
58
+ data = self._request("GET", "/{}/dns_records".format(zone_id), type=record_type, per_page=10000, **query)
59
+ record = next((r for r in data if r.get("name") == name and r.get("type") == record_type), None)
60
+ self.logger.debug("Record queried: %s", record)
61
+ if record:
62
+ return record
63
+ self.logger.warning("Failed to query record: %s", data)
64
+ return None
65
+
66
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
67
+ # type: (str, str, str, str, str, int | str | None, str | None, dict ) -> bool
68
+ """https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/"""
69
+ name = join_domain(subdomain, main_domain)
70
+ extra["comment"] = extra.get("comment", self.remark) # 添加注释
71
+ data = self._request(
72
+ "POST", "/{}/dns_records".format(zone_id), name=name, type=record_type, content=value, ttl=ttl, **extra
73
+ )
74
+ if data:
75
+ self.logger.info("Record created: %s", data)
76
+ return True
77
+ self.logger.error("Failed to create record: %s", data)
78
+ return False
79
+
80
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
81
+ # type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
82
+ """https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/"""
83
+ extra["comment"] = extra.get("comment", self.remark) # 注释
84
+ extra["proxied"] = old_record.get("proxied", extra.get("proxied")) # 保持原有的代理状态
85
+ extra["tags"] = old_record.get("tags", extra.get("tags")) # 保持原有的标签
86
+ extra["settings"] = old_record.get("settings", extra.get("settings")) # 保持原有的设置
87
+ data = self._request(
88
+ "PUT",
89
+ "/{}/dns_records/{}".format(zone_id, old_record["id"]),
90
+ type=record_type,
91
+ name=old_record.get("name"),
92
+ content=value,
93
+ ttl=ttl,
94
+ **extra
95
+ )
96
+ self.logger.debug("Record updated: %s", data)
97
+ if data:
98
+ return True
99
+ return False
ddns/provider/debug.py ADDED
@@ -0,0 +1,20 @@
1
+ # coding=utf-8
2
+ """
3
+ DebugProvider
4
+ 仅打印出 IP 地址,不进行任何实际 DNS 更新。
5
+ """
6
+
7
+ from ._base import SimpleProvider
8
+
9
+
10
+ class DebugProvider(SimpleProvider):
11
+
12
+ def _validate(self):
13
+ """无需任何验证"""
14
+ pass
15
+
16
+ def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
17
+ self.logger.debug("DebugProvider: %s(%s) => %s", domain, record_type, value)
18
+ ip_type = "IPv4" if record_type == "A" else "IPv6" if record_type == "AAAA" else record_type
19
+ print("[{}] {}".format(ip_type, value))
20
+ return True
ddns/provider/dnscom.py CHANGED
@@ -1,180 +1,102 @@
1
1
  # coding=utf-8
2
2
  """
3
- DNSCOM API
4
- DNS.COM 接口解析操作库
5
- http://open.dns.com/
6
- @author: Bigjin
7
- @mailto: i@bigjin.com
3
+ DNSCOM/51dns API 接口解析操作库
4
+ www.51dns.com (原dns.com)
5
+ @author: Bigjin<i@bigjin.com>, NewFuture
8
6
  """
9
7
 
8
+ from ._base import BaseProvider, TYPE_FORM
10
9
  from hashlib import md5
11
- from json import loads as jsondecode
12
- from logging import debug, info, warning
13
- from time import mktime
14
- from datetime import datetime
10
+ from time import time
15
11
 
16
- try: # python 3
17
- from http.client import HTTPSConnection
18
- from urllib.parse import urlencode
19
- except ImportError: # python 2
20
- from httplib import HTTPSConnection
21
- from urllib import urlencode
22
12
 
23
- __author__ = 'Bigjin'
24
- # __all__ = ["request", "ID", "TOKEN", "PROXY"]
25
-
26
-
27
- class Config:
28
- ID = "id"
29
- TOKEN = "TOKEN"
30
- PROXY = None # 代理设置
31
- TTL = None
32
-
33
-
34
- class API:
35
- # API 配置
36
- SITE = "www.dns.com" # API endpoint
37
- METHOD = "POST" # 请求方法
38
-
39
-
40
- def signature(params):
13
+ class DnscomProvider(BaseProvider):
41
14
  """
42
- 计算签名,返回签名后的查询参数
15
+ DNSCOM/51dns API Provider
16
+ https://www.51dns.com/document/api/index.html
43
17
  """
44
- params.update({
45
- 'apiKey': Config.ID,
46
- 'timestamp': mktime(datetime.now().timetuple()),
47
- })
48
- query = urlencode(sorted(params.items()))
49
- debug(query)
50
- sign = query
51
- debug("signString: %s", sign)
52
-
53
- sign = md5((sign + Config.TOKEN).encode('utf-8')).hexdigest()
54
- params["hash"] = sign
55
-
56
- return params
57
-
58
18
 
59
- def request(action, param=None, **params):
60
- """
61
- 发送请求数据
62
- """
63
- if param:
64
- params.update(param)
65
- params = dict((k, params[k]) for k in params if params[k] is not None)
66
- params = signature(params)
67
- info("%s/api/%s/ : params:%s", API.SITE, action, params)
68
-
69
- if Config.PROXY:
70
- conn = HTTPSConnection(Config.PROXY)
71
- conn.set_tunnel(API.SITE, 443)
72
- else:
73
- conn = HTTPSConnection(API.SITE)
74
-
75
- conn.request(API.METHOD, '/api/' + action + '/', urlencode(params),
76
- {"Content-type": "application/x-www-form-urlencoded"})
77
- response = conn.getresponse()
78
- result = response.read().decode('utf8')
79
- conn.close()
80
-
81
- if response.status < 200 or response.status >= 300:
82
- warning('%s : error[%d]:%s', action, response.status, result)
83
- raise Exception(result)
84
- else:
85
- data = jsondecode(result)
86
- debug('%s : result:%s', action, data)
87
- if data.get('code') != 0:
88
- raise Exception("api error:", data.get('message'))
89
- data = data.get('data')
90
- if data is None:
91
- raise Exception('response data is none')
92
- return data
93
-
94
-
95
- def get_domain_info(domain):
96
- """
97
- 切割域名获取主域名和对应ID
98
- """
99
- if len(domain.split('.')) > 2:
100
- domains = domain.split('.', 1)
101
- sub = domains[0]
102
- main = domains[1]
103
- else:
104
- sub = '' # 接口有bug 不能传 @ * 作为主机头,但是如果为空,默认为 @
105
- main = domain
106
-
107
- res = request("domain/getsingle", domainID=main)
108
- domain_id = res.get('domainID')
109
- return sub, main, domain_id
110
-
111
-
112
- def get_records(domain, domain_id, **conditions):
113
- """
114
- 获取记录ID
115
- 返回满足条件的所有记录[]
116
- TODO 大于500翻页
117
- """
118
- if not hasattr(get_records, "records"):
119
- get_records.records = {} # "静态变量"存储已查询过的id
120
- get_records.keys = ("recordID", "record", "type", "viewID",
121
- "TTL", "state", "value")
122
-
123
- if domain not in get_records.records:
124
- get_records.records[domain] = {}
125
- data = request("record/list",
126
- domainID=domain_id, pageSize=500)
127
- if data.get('data'):
128
- for record in data.get('data'):
129
- get_records.records[domain][record["recordID"]] = {
130
- k: v for (k, v) in record.items() if k in get_records.keys}
131
- records = {}
132
- for (rid, record) in get_records.records[domain].items():
133
- for (k, value) in conditions.items():
134
- if record.get(k) != value:
135
- break
136
- else: # for else push
137
- records[rid] = record
138
- return records
139
-
140
-
141
- def update_record(domain, value, record_type='A'):
142
- """
143
- 更新记录
144
- """
145
- info(">>>>>%s(%s)", domain, record_type)
146
- sub, main, domain_id = get_domain_info(domain)
147
-
148
- records = get_records(main, domain_id, record=sub, type=record_type)
149
- result = {}
150
-
151
- if records:
152
- for (rid, record) in records.items():
153
- if record["value"] != value:
154
- debug(sub, record)
155
- res = request("record/modify", domainID=domain_id,
156
- recordID=rid, newvalue=value, newTTL=Config.TTL)
157
- if res:
158
- # update records
159
- get_records.records[main][rid]["value"] = value
160
- result[rid] = res
161
- else:
162
- result[rid] = "update fail!\n" + str(res)
163
- else:
164
- result[rid] = domain
165
- else:
166
- res = request("record/create", domainID=domain_id,
167
- value=value, host=sub, type=record_type, TTL=Config.TTL)
168
- if res:
169
- # update records INFO
170
- rid = res.get('recordID')
171
- get_records.records[main][rid] = {
172
- 'value': value,
173
- "recordID": rid,
174
- "record": sub,
175
- "type": record_type
19
+ API = "https://www.51dns.com"
20
+ content_type = TYPE_FORM
21
+
22
+ def _validate(self):
23
+ self.logger.warning(
24
+ "DNS.COM 缺少充分的真实环境测试,请及时在 GitHub Issues 中反馈: %s",
25
+ "https://github.com/NewFuture/DDNS/issues",
26
+ )
27
+ super(DnscomProvider, self)._validate()
28
+
29
+ def _signature(self, params):
30
+ """https://www.51dns.com/document/api/70/72.html"""
31
+ params = {k: v for k, v in params.items() if v is not None}
32
+ params.update(
33
+ {
34
+ "apiKey": self.auth_id,
35
+ "timestamp": time(), # 时间戳
176
36
  }
177
- result = res
178
- else:
179
- result = domain + " created fail!"
180
- return result
37
+ )
38
+ query = self._encode(sorted(params.items()))
39
+ sign = md5((query + self.auth_token).encode("utf-8")).hexdigest()
40
+ params["hash"] = sign
41
+ return params
42
+
43
+ def _request(self, action, **params):
44
+ params = self._signature(params)
45
+ data = self._http("POST", "/api/{}/".format(action), body=params)
46
+ if data is None or not isinstance(data, dict):
47
+ raise Exception("response data is none")
48
+ if data.get("code", 0) != 0:
49
+ raise Exception("api error: " + str(data.get("message")))
50
+ return data.get("data")
51
+
52
+ def _query_zone_id(self, domain):
53
+ """https://www.51dns.com/document/api/74/31.html"""
54
+ res = self._request("domain/getsingle", domainID=domain)
55
+ self.logger.debug("Queried domain: %s", res)
56
+ if res:
57
+ return res.get("domainID")
58
+ return None
59
+
60
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
61
+ """https://www.51dns.com/document/api/4/47.html"""
62
+ records = self._request("record/list", domainID=zone_id, host=subdomain, pageSize=500)
63
+ records = records.get("data", []) if records else []
64
+ for record in records:
65
+ if (
66
+ record.get("record") == subdomain
67
+ and record.get("type") == record_type
68
+ and (line is None or record.get("viewID") == line)
69
+ ):
70
+ return record
71
+ return None
72
+
73
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
74
+ """https://www.51dns.com/document/api/4/12.html"""
75
+ extra["remark"] = extra.get("remark", self.remark)
76
+ res = self._request(
77
+ "record/create",
78
+ domainID=zone_id,
79
+ value=value,
80
+ host=subdomain,
81
+ type=record_type,
82
+ TTL=ttl,
83
+ viewID=line,
84
+ **extra
85
+ )
86
+ if res and res.get("recordID"):
87
+ self.logger.info("Record created: %s", res)
88
+ return True
89
+ self.logger.error("Failed to create record: %s", res)
90
+ return False
91
+
92
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
93
+ """https://www.51dns.com/document/api/4/45.html"""
94
+ extra["remark"] = extra.get("remark", self.remark)
95
+ res = self._request(
96
+ "record/modify", domainID=zone_id, recordID=old_record.get("recordID"), newvalue=value, newTTL=ttl
97
+ )
98
+ if res:
99
+ self.logger.info("Record updated: %s", res)
100
+ return True
101
+ self.logger.error("Failed to update record: %s", res)
102
+ return False