ddns 4.1.0b1__py2.py3-none-any.whl → 4.1.0b3__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/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
@@ -5,9 +5,11 @@ Tencent Cloud DNSPod API
5
5
 
6
6
  @author: NewFuture
7
7
  """
8
- from ._base import BaseProvider, TYPE_JSON, hmac_sha256_authorization, sha256_hash, hmac_sha256
9
- from time import time, strftime, gmtime
10
- from json import dumps as jsonencode
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
11
13
 
12
14
 
13
15
  class TencentCloudProvider(BaseProvider):
@@ -19,7 +21,7 @@ class TencentCloudProvider(BaseProvider):
19
21
  Documentation: https://cloud.tencent.com/document/api/1427
20
22
  """
21
23
 
22
- API = "https://dnspod.tencentcloudapi.com"
24
+ endpoint = "https://dnspod.tencentcloudapi.com"
23
25
  content_type = TYPE_JSON
24
26
 
25
27
  # 腾讯云 DNSPod API 配置
@@ -42,26 +44,23 @@ class TencentCloudProvider(BaseProvider):
42
44
  """
43
45
  # 构建请求体
44
46
  params = {k: v for k, v in params.items() if v is not None}
45
- body = jsonencode(params)
47
+ body = self._encode_body(params)
46
48
 
47
49
  # 构建请求头,小写 腾讯云只签名特定头部
48
- headers = {
49
- "content-type": self.content_type,
50
- "host": self.API.split("://", 1)[1].strip("/"),
51
- }
50
+ headers = {"content-type": self.content_type, "host": self.endpoint.split("://", 1)[1].strip("/")}
52
51
 
53
52
  # 腾讯云特殊的密钥派生过程
54
53
  date = strftime("%Y-%m-%d", gmtime())
55
54
  credential_scope = "{}/{}/tc3_request".format(date, self.service)
56
55
 
57
56
  # 派生签名密钥
58
- secret_date = hmac_sha256("TC3" + self.auth_token, date).digest()
57
+ secret_date = hmac_sha256("TC3" + self.token, date).digest()
59
58
  secret_service = hmac_sha256(secret_date, self.service).digest()
60
59
  signing_key = hmac_sha256(secret_service, "tc3_request").digest()
61
60
 
62
61
  # 预处理模板字符串
63
62
  auth_format = "TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
64
- self.auth_id,
63
+ self.id,
65
64
  credential_scope,
66
65
  )
67
66
  timestamp = str(int(time()))
@@ -132,7 +131,7 @@ class TencentCloudProvider(BaseProvider):
132
131
  RecordType=record_type,
133
132
  RecordLine=line,
134
133
  **extra
135
- )
134
+ ) # fmt: skip
136
135
  if not response or "RecordList" not in response:
137
136
  self.logger.debug("No records found or query failed")
138
137
  return None
@@ -165,7 +164,7 @@ class TencentCloudProvider(BaseProvider):
165
164
  RecordLine=line or "默认",
166
165
  TTL=int(ttl) if ttl else None,
167
166
  **extra
168
- )
167
+ ) # fmt: skip
169
168
  if response and "RecordId" in response:
170
169
  self.logger.info("Record created successfully with ID: %s", response["RecordId"])
171
170
  return True
@@ -186,7 +185,7 @@ class TencentCloudProvider(BaseProvider):
186
185
  Value=value,
187
186
  TTL=int(ttl) if ttl else None,
188
187
  **extra
189
- )
188
+ ) # fmt: skip
190
189
  if response and "RecordId" in response:
191
190
  self.logger.info("Record updated successfully")
192
191
  return True
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 | None
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
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