ddns 4.0.1__py2.py3-none-any.whl → 4.1.0__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.

@@ -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,101 @@
1
+ # coding=utf-8
2
+ """
3
+ No-IP (noip.com) Dynamic DNS API
4
+ @author: GitHub Copilot
5
+ """
6
+
7
+ from ._base import SimpleProvider, TYPE_FORM, quote
8
+
9
+
10
+ class NoipProvider(SimpleProvider):
11
+ """
12
+ No-IP (www.noip.com) Dynamic DNS Provider
13
+
14
+ No-IP is a popular dynamic DNS service that provides simple HTTP-based
15
+ API for updating DNS records. This provider supports the standard
16
+ No-IP update protocol.
17
+ """
18
+
19
+ endpoint = "https://dynupdate.no-ip.com"
20
+ content_type = TYPE_FORM
21
+ accept = None # No-IP returns plain text response
22
+ decode_response = False # Response is plain text, not JSON
23
+
24
+ def _validate(self):
25
+ """
26
+ Validate authentication credentials for No-IP and update endpoint with auth
27
+ """
28
+ # Check endpoint first
29
+ if not self.endpoint or "://" not in self.endpoint:
30
+ raise ValueError("API endpoint must be defined and contain protocol")
31
+
32
+ if not self.id:
33
+ raise ValueError("No-IP requires username as 'id'")
34
+ if not self.token:
35
+ raise ValueError("No-IP requires password as 'token'")
36
+
37
+ # Update endpoint with URL-encoded auth credentials
38
+ protocol, domain = self.endpoint.split("://", 1)
39
+ self.endpoint = "{0}://{1}:{2}@{3}".format(
40
+ protocol, quote(self.id, safe=""), quote(self.token, safe=""), domain
41
+ )
42
+
43
+ def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
44
+ """
45
+ Update DNS record using No-IP Dynamic Update API
46
+
47
+ No-IP API Reference:
48
+ - URL: https://dynupdate.no-ip.com/nic/update
49
+ - Method: GET or POST
50
+ - Authentication: HTTP Basic Auth (username:password)
51
+ - Parameters:
52
+ - hostname: The hostname to update
53
+ - myip: The IP address to set (optional, uses client IP
54
+ if not provided)
55
+
56
+ Response codes:
57
+ - good: Update successful
58
+ - nochg: IP address is current, no update performed
59
+ - nohost: Hostname supplied does not exist
60
+ - badauth: Invalid username/password combination
61
+ - badagent: Client disabled
62
+ - !donator: An update request was sent including a feature that
63
+ is not available
64
+ - abuse: Username is blocked due to abuse
65
+ """
66
+ self.logger.info("%s => %s(%s)", domain, value, record_type)
67
+
68
+ # Prepare request parameters
69
+ params = {"hostname": domain, "myip": value}
70
+
71
+ try:
72
+ # Use GET request as it's the most common method for DDNS
73
+ # Endpoint already includes auth credentials from _validate()
74
+ response = self._http("GET", "/nic/update", queries=params)
75
+
76
+ if response is not None:
77
+ response_str = str(response).strip()
78
+ self.logger.info("No-IP API response: %s", response_str)
79
+
80
+ # Check for successful responses
81
+ if response_str.startswith("good") or response_str.startswith("nochg"):
82
+ return True
83
+ elif response_str.startswith("nohost"):
84
+ self.logger.error("Hostname %s does not exist under No-IP account", domain)
85
+ elif response_str.startswith("badauth"):
86
+ self.logger.error("Invalid No-IP username/password combination")
87
+ elif response_str.startswith("badagent"):
88
+ self.logger.error("No-IP client disabled")
89
+ elif response_str.startswith("!donator"):
90
+ self.logger.error("Feature not available for No-IP free account")
91
+ elif response_str.startswith("abuse"):
92
+ self.logger.error("No-IP account blocked due to abuse")
93
+ else:
94
+ self.logger.error("Unexpected No-IP API response: %s", response_str)
95
+ else:
96
+ self.logger.error("Empty response from No-IP API")
97
+
98
+ except Exception as e:
99
+ self.logger.error("Error updating No-IP record for %s: %s", domain, e)
100
+
101
+ return False
@@ -0,0 +1,194 @@
1
+ # coding=utf-8
2
+ """
3
+ Tencent Cloud DNSPod API
4
+ 腾讯云 DNSPod API
5
+
6
+ @author: NewFuture
7
+ """
8
+
9
+ from time import gmtime, strftime, time
10
+
11
+ from ._base import TYPE_JSON, BaseProvider
12
+ from ._signature import hmac_sha256, hmac_sha256_authorization, sha256_hash
13
+
14
+
15
+ class TencentCloudProvider(BaseProvider):
16
+ """
17
+ 腾讯云 DNSPod API 提供商
18
+ Tencent Cloud DNSPod API Provider
19
+
20
+ API Version: 2021-03-23
21
+ Documentation: https://cloud.tencent.com/document/api/1427
22
+ """
23
+
24
+ endpoint = "https://dnspod.tencentcloudapi.com"
25
+ content_type = TYPE_JSON
26
+
27
+ # 腾讯云 DNSPod API 配置
28
+ service = "dnspod"
29
+ version_date = "2021-03-23"
30
+
31
+ def _request(self, action, **params):
32
+ # type: (str, **(str | int | bytes | bool | None)) -> dict | None
33
+ """
34
+ 发送腾讯云 API 请求
35
+
36
+ API 文档: https://cloud.tencent.com/document/api/1427/56187
37
+
38
+ Args:
39
+ action (str): API 操作名称
40
+ params (dict): 请求参数
41
+
42
+ Returns:
43
+ dict: API 响应结果
44
+ """
45
+ # 构建请求体
46
+ params = {k: v for k, v in params.items() if v is not None}
47
+ body = self._encode_body(params)
48
+
49
+ # 构建请求头,小写 腾讯云只签名特定头部
50
+ headers = {"content-type": self.content_type, "host": self.endpoint.split("://", 1)[1].strip("/")}
51
+
52
+ # 腾讯云特殊的密钥派生过程
53
+ date = strftime("%Y-%m-%d", gmtime())
54
+ credential_scope = "{}/{}/tc3_request".format(date, self.service)
55
+
56
+ # 派生签名密钥
57
+ secret_date = hmac_sha256("TC3" + self.token, date).digest()
58
+ secret_service = hmac_sha256(secret_date, self.service).digest()
59
+ signing_key = hmac_sha256(secret_service, "tc3_request").digest()
60
+
61
+ # 预处理模板字符串
62
+ auth_format = "TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
63
+ self.id,
64
+ credential_scope,
65
+ )
66
+ timestamp = str(int(time()))
67
+ sign_template = "\n".join(["TC3-HMAC-SHA256", timestamp, credential_scope, "{HashedCanonicalRequest}"])
68
+ authorization = hmac_sha256_authorization(
69
+ secret_key=signing_key,
70
+ method="POST",
71
+ path="/",
72
+ query="",
73
+ headers=headers,
74
+ body_hash=sha256_hash(body),
75
+ signing_string_format=sign_template,
76
+ authorization_format=auth_format,
77
+ )
78
+ # X-TC 更新签名之后方可添加
79
+ headers.update(
80
+ {
81
+ "X-TC-Action": action,
82
+ "X-TC-Version": self.version_date,
83
+ "X-TC-Timestamp": timestamp,
84
+ "authorization": authorization,
85
+ }
86
+ )
87
+
88
+ response = self._http("POST", "/", body=body, headers=headers)
89
+ if response and "Response" in response:
90
+ if "Error" in response["Response"]:
91
+ error = response["Response"]["Error"]
92
+ self.logger.error(
93
+ "TencentCloud API error: %s - %s",
94
+ error.get("Code", "Unknown"),
95
+ error.get("Message", "Unknown error"),
96
+ )
97
+ return None
98
+ return response["Response"]
99
+
100
+ self.logger.warning("Unexpected response format: %s", response)
101
+ return None
102
+
103
+ def _query_zone_id(self, domain):
104
+ # type: (str) -> str | None
105
+ """查询域名的 zone_id (domain id) https://cloud.tencent.com/document/api/1427/56173"""
106
+ # 使用 DescribeDomain API 查询指定域名的信息
107
+ response = self._request("DescribeDomain", Domain=domain)
108
+
109
+ if not response or "DomainInfo" not in response:
110
+ self.logger.debug("Domain info not found or query failed for: %s", domain)
111
+ return None
112
+
113
+ domain_id = response.get("DomainInfo", {}).get("DomainId")
114
+
115
+ if domain_id is not None:
116
+ self.logger.debug("Found domain %s with ID: %s", domain, domain_id)
117
+ return str(domain_id)
118
+
119
+ self.logger.debug("Domain ID not found in response for: %s", domain)
120
+ return None
121
+
122
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
123
+ # type: (str, str, str, str, str | None, dict) -> dict | None
124
+ """查询 DNS 记录列表 https://cloud.tencent.com/document/api/1427/56166"""
125
+
126
+ response = self._request(
127
+ "DescribeRecordList",
128
+ DomainId=int(zone_id),
129
+ Subdomain=subdomain,
130
+ Domain=main_domain,
131
+ RecordType=record_type,
132
+ RecordLine=line,
133
+ **extra
134
+ ) # fmt: skip
135
+ if not response or "RecordList" not in response:
136
+ self.logger.debug("No records found or query failed")
137
+ return None
138
+
139
+ records = response["RecordList"]
140
+ if not records:
141
+ self.logger.debug("No records found for subdomain: %s", subdomain)
142
+ return None
143
+
144
+ # 查找匹配的记录
145
+ target_name = subdomain if subdomain and subdomain != "@" else "@"
146
+ for record in records:
147
+ if record.get("Name") == target_name and record.get("Type") == record_type.upper():
148
+ self.logger.debug("Found existing record: %s", record)
149
+ return record
150
+
151
+ self.logger.debug("No matching record found")
152
+ return None
153
+
154
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
155
+ """创建 DNS 记录 https://cloud.tencent.com/document/api/1427/56180"""
156
+ extra["Remark"] = extra.get("Remark", self.remark)
157
+ response = self._request(
158
+ "CreateRecord",
159
+ Domain=main_domain,
160
+ DomainId=int(zone_id),
161
+ SubDomain=subdomain,
162
+ RecordType=record_type,
163
+ Value=value,
164
+ RecordLine=line or "默认",
165
+ TTL=int(ttl) if ttl else None,
166
+ **extra
167
+ ) # fmt: skip
168
+ if response and "RecordId" in response:
169
+ self.logger.info("Record created successfully with ID: %s", response["RecordId"])
170
+ return True
171
+ self.logger.error("Failed to create record:\n%s", response)
172
+ return False
173
+
174
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
175
+ """更新 DNS 记录: https://cloud.tencent.com/document/api/1427/56157"""
176
+ extra["Remark"] = extra.get("Remark", self.remark)
177
+ response = self._request(
178
+ "ModifyRecord",
179
+ Domain=old_record.get("Domain", ""),
180
+ DomainId=old_record.get("DomainId", int(zone_id)),
181
+ SubDomain=old_record.get("Name"),
182
+ RecordId=old_record.get("RecordId"),
183
+ RecordType=record_type,
184
+ RecordLine=old_record.get("Line", line or "默认"),
185
+ Value=value,
186
+ TTL=int(ttl) if ttl else None,
187
+ **extra
188
+ ) # fmt: skip
189
+ if response and "RecordId" in response:
190
+ self.logger.info("Record updated successfully")
191
+ return True
192
+
193
+ self.logger.error("Failed to update record")
194
+ return False
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/fileio.py ADDED
@@ -0,0 +1,113 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ File I/O utilities for DDNS with Python 2/3 compatibility
4
+ @author: NewFuture
5
+ """
6
+
7
+ import os
8
+ from io import open # Python 2/3 compatible UTF-8 file operations
9
+
10
+
11
+ def _ensure_directory_exists(file_path): # type: (str) -> None
12
+ """
13
+ Internal helper to ensure directory exists for the given file path
14
+
15
+ Args:
16
+ file_path (str): File path whose directory should be created
17
+
18
+ Raises:
19
+ OSError: If directory cannot be created
20
+ """
21
+ directory = os.path.dirname(file_path)
22
+ if directory and not os.path.exists(directory):
23
+ os.makedirs(directory)
24
+
25
+
26
+ def read_file_safely(file_path, encoding="utf-8", default=None): # type: (str, str, str|None) -> str
27
+ """
28
+ Safely read file content with UTF-8 encoding, return None if file doesn't exist or can't be read
29
+
30
+ Args:
31
+ file_path (str): Path to the file to read
32
+ encoding (str): File encoding (default: utf-8)
33
+
34
+ Returns:
35
+ str | None: File content or None if failed
36
+ """
37
+ try:
38
+ return read_file(file_path, encoding)
39
+ except Exception:
40
+ return default # type: ignore
41
+
42
+
43
+ def write_file_safely(file_path, content, encoding="utf-8"): # type: (str, str, str) -> bool
44
+ """
45
+ Safely write content to file with UTF-8 encoding
46
+
47
+ Args:
48
+ file_path (str): Path to the file to write
49
+ content (str): Content to write
50
+ encoding (str): File encoding (default: utf-8)
51
+
52
+ Returns:
53
+ bool: True if write successful, False otherwise
54
+ """
55
+ try:
56
+ write_file(file_path, content, encoding)
57
+ return True
58
+ except Exception:
59
+ return False
60
+
61
+
62
+ def read_file(file_path, encoding="utf-8"): # type: (str, str) -> str
63
+ """
64
+ Read file content with UTF-8 encoding, raise exception if failed
65
+
66
+ Args:
67
+ file_path (str): Path to the file to read
68
+ encoding (str): File encoding (default: utf-8)
69
+
70
+ Returns:
71
+ str: File content
72
+
73
+ Raises:
74
+ IOError: If file cannot be read
75
+ UnicodeDecodeError: If file cannot be decoded with specified encoding
76
+ """
77
+ with open(file_path, "r", encoding=encoding) as f:
78
+ return f.read()
79
+
80
+
81
+ def write_file(file_path, content, encoding="utf-8"): # type: (str, str, str) -> None
82
+ """
83
+ Write content to file with UTF-8 encoding, raise exception if failed
84
+
85
+ Args:
86
+ file_path (str): Path to the file to write
87
+ content (str): Content to write
88
+ encoding (str): File encoding (default: utf-8)
89
+
90
+ Raises:
91
+ IOError: If file cannot be written
92
+ UnicodeEncodeError: If content cannot be encoded with specified encoding
93
+ """
94
+ _ensure_directory_exists(file_path)
95
+ with open(file_path, "w", encoding=encoding) as f:
96
+ f.write(content)
97
+
98
+
99
+ def ensure_directory(file_path): # type: (str) -> bool
100
+ """
101
+ Ensure the directory for the given file path exists
102
+
103
+ Args:
104
+ file_path (str): File path whose directory should be created
105
+
106
+ Returns:
107
+ bool: True if directory exists or was created successfully
108
+ """
109
+ try:
110
+ _ensure_directory_exists(file_path)
111
+ return True
112
+ except (OSError, IOError):
113
+ return False