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.
@@ -2,264 +2,141 @@
2
2
  """
3
3
  HuaweiDNS API
4
4
  华为DNS解析操作库
5
- https://support.huaweicloud.com/api-dns/zh-cn_topic_0037134406.html
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
- from hashlib import sha256
11
- from hmac import new as hmac
12
- from binascii import hexlify
13
- from json import loads as jsondecode, dumps as jsonencode
14
- from logging import debug, info, warning
15
- from datetime import datetime
16
-
17
- try: # python 3
18
- from http.client import HTTPSConnection
19
- from urllib.parse import urlencode
20
- except ImportError: # python 2
21
- from httplib import HTTPSConnection
22
- from urllib import urlencode
23
-
24
-
25
- __author__ = 'New Future'
26
- BasicDateFormat = "%Y%m%dT%H%M%SZ"
27
- Algorithm = "SDK-HMAC-SHA256"
28
-
29
-
30
- # __all__ = ["request", "ID", "TOKEN", "PROXY"]
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
- def get_zone_id(domain):
144
- """
145
- 切割域名获取主域名和对应ID https://support.huaweicloud.com/api-dns/dns_api_62003.html
146
- 优先匹配级数最长的主域名
147
- """
148
- zoneid = None
149
- domain_slice = domain.split('.')
150
- index = len(domain_slice)
151
- root_domain = '.'.join(domain_slice[-2:])
152
- zones = request('GET', '/v2/zones', limit=500, name=root_domain)['zones']
153
- while (not zoneid) and (index >= 2):
154
- domain = '.'.join(domain_slice[-index:]) + '.'
155
- zone = next((z for z in zones if domain == (z.get('name'))), None)
156
- zoneid = zone and zone['id']
157
- index -= 1
158
- return zoneid
159
-
160
-
161
- def get_records(zoneid, **conditions):
162
- """
163
- 获取记录ID
164
- 返回满足条件的所有记录[]
165
- https://support.huaweicloud.com/api-dns/dns_api_64004.html
166
- TODO 大于500翻页
167
- """
168
- cache_key = zoneid + "_" + \
169
- conditions.get('name', "") + "_" + conditions.get('type', "")
170
- if not hasattr(get_records, 'records'):
171
- get_records.records = {} # "静态变量"存储已查询过的id
172
- get_records.keys = ('id', 'type', 'name', 'records', 'ttl')
173
-
174
- if zoneid not in get_records.records:
175
- get_records.records[cache_key] = {}
176
-
177
- data = request('GET', '/v2/zones/' + zoneid + '/recordsets',
178
- limit=500, **conditions)
179
-
180
- # https://{DNS_Endpoint}/v2/zones/2c9eb155587194ec01587224c9f90149/recordsets?limit=&offset=
181
- if data:
182
- for record in data['recordsets']:
183
- info(record)
184
- get_records.records[cache_key][record['id']] = {
185
- k: v for (k, v) in record.items() if k in get_records.keys}
186
- records = {}
187
- for (zid, record) in get_records.records[cache_key].items():
188
- for (k, value) in conditions.items():
189
- if record.get(k) != value:
190
- break
191
- else: # for else push
192
- records[zid] = record
193
- return records
194
-
195
-
196
- def update_record(domain, value, record_type='A'):
197
- """
198
- 更新记录
199
- update
200
- https://support.huaweicloud.com/api-dns/UpdateRecordSet.html
201
- add
202
- https://support.huaweicloud.com/api-dns/dns_api_64001.html
203
- """
204
- info(">>>>>%s(%s)", domain, record_type)
205
- zoneid = get_zone_id(domain)
206
- if not zoneid:
207
- raise Exception("invalid domain: [ %s ] " % domain)
208
- domain += '.'
209
- records = get_records(zoneid, name=domain, type=record_type)
210
- cache_key = zoneid + "_" + domain + "_" + record_type
211
- result = {}
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