ddns 4.1.0b2__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/util/http.py CHANGED
@@ -1,156 +1,91 @@
1
1
  # coding=utf-8
2
2
  """
3
- HTTP请求工具模块
4
-
5
- HTTP utilities module for DDNS project.
6
- Provides common HTTP functionality including redirect following support.
3
+ HTTP请求工具模块,SSL 代理,重试,basic-auth
4
+ HTTP utilities module, including ssl, proxies, retry and basicAuth.
7
5
 
8
6
  @author: NewFuture
9
7
  """
10
8
 
11
9
  from logging import getLogger
10
+ from re import compile
12
11
  import ssl
13
12
  import os
13
+ import time
14
+ import socket
15
+
16
+ from .. import __version__
14
17
 
15
18
  try: # python 3
16
- from urllib.request import Request, HTTPSHandler, ProxyHandler, build_opener, OpenerDirector # noqa: F401
17
- from urllib.parse import quote, urlencode
18
- from urllib.error import HTTPError, URLError
19
+ from urllib.request import (
20
+ BaseHandler,
21
+ build_opener,
22
+ HTTPBasicAuthHandler,
23
+ HTTPDefaultErrorHandler,
24
+ HTTPPasswordMgrWithDefaultRealm,
25
+ HTTPSHandler,
26
+ ProxyHandler,
27
+ Request,
28
+ )
29
+ from urllib.parse import quote, urlencode, unquote
30
+ from http.client import HTTPSConnection
19
31
  except ImportError: # python 2
20
- from urllib2 import Request, HTTPSHandler, ProxyHandler, build_opener, HTTPError, URLError # type: ignore[no-redef]
21
- from urllib import urlencode, quote # type: ignore[no-redef]
22
-
23
- __all__ = [
24
- "send_http_request",
25
- "HttpResponse",
26
- "quote",
27
- "urlencode",
28
- "URLError",
29
- ]
30
-
31
- logger = getLogger().getChild(__name__)
32
-
33
-
34
- class HttpResponse(object):
35
- """HTTP响应封装类"""
36
-
37
- def __init__(self, status, reason, headers, body):
38
- # type: (int, str, object, str) -> None
39
- """
40
- 初始化HTTP响应对象
41
-
42
- Args:
43
- status (int): HTTP状态码
44
- reason (str): 状态原因短语
45
- headers (object): 响应头对象,直接使用 response.info()
46
- body (str): 响应体内容
47
- """
48
- self.status = status
49
- self.reason = reason
50
- self.headers = headers
51
- self.body = body
52
-
53
- def get_header(self, name, default=None):
54
- # type: (str, str | None) -> str | None
55
- """
56
- 获取指定名称的头部值
57
-
58
- Args:
59
- name (str): 头部名称
60
- default (str | None): 默认值
61
-
62
- Returns:
63
- str | None: 头部值,如果不存在则返回默认值
64
- """
65
- return self.headers.get(name, default) # type: ignore[union-attr]
66
-
67
-
68
- # 移除了自定义重定向处理器,使用urllib2/urllib.request的内置重定向处理
69
-
70
-
71
- def _create_ssl_context(verify_ssl):
72
- # type: (bool | str) -> ssl.SSLContext | None
73
- """创建SSL上下文"""
74
- ssl_context = ssl.create_default_context()
75
-
76
- if verify_ssl is False:
77
- # 禁用SSL验证
78
- ssl_context.check_hostname = False
79
- ssl_context.verify_mode = ssl.CERT_NONE
80
- elif hasattr(verify_ssl, "lower") and verify_ssl.lower() not in ("auto", "true"): # type: ignore[union-attr]
81
- # 使用自定义CA证书
82
- try:
83
- ssl_context.load_verify_locations(verify_ssl) # type: ignore[arg-type]
84
- except Exception as e:
85
- logger.error("Failed to load CA certificate from %s: %s", verify_ssl, e)
86
- return None
87
- else:
88
- # 默认验证,尝试加载系统证书
89
- _load_system_ca_certs(ssl_context)
90
-
91
- return ssl_context
92
-
93
-
94
- def _load_system_ca_certs(ssl_context):
95
- # type: (ssl.SSLContext) -> None
96
- """加载系统CA证书"""
97
- # 常见CA证书路径
98
- ca_paths = [
99
- # Linux/Unix常用路径
100
- "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
101
- "/etc/pki/tls/certs/ca-bundle.crt", # RedHat/CentOS
102
- "/etc/ssl/ca-bundle.pem", # OpenSUSE
103
- "/etc/ssl/cert.pem", # OpenBSD
104
- "/usr/local/share/certs/ca-root-nss.crt", # FreeBSD
105
- "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", # Fedora/RHEL
106
- # macOS路径
107
- "/usr/local/etc/openssl/cert.pem", # macOS with Homebrew
108
- "/opt/local/etc/openssl/cert.pem", # macOS with MacPorts
109
- ]
110
-
111
- # Windows额外路径
112
- if os.name == "nt":
113
- ca_paths.append("C:\\Program Files\\Git\\mingw64\\ssl\\cert.pem")
114
- ca_paths.append("C:\\Program Files\\OpenSSL\\ssl\\cert.pem")
115
-
116
- loaded_count = 0
117
- for ca_path in ca_paths:
118
- if os.path.isfile(ca_path):
119
- try:
120
- ssl_context.load_verify_locations(ca_path)
121
- loaded_count += 1
122
- logger.debug("Loaded CA certificates from: %s", ca_path)
123
- except Exception as e:
124
- logger.info("Failed to load CA certificates from %s: %s", ca_path, e)
125
-
126
-
127
- def _create_opener(proxy, verify_ssl):
128
- # type: (str | None, bool | str) -> OpenerDirector
129
- """创建URL打开器,支持代理和SSL配置"""
130
- handlers = []
131
-
132
- if proxy:
133
- handlers.append(ProxyHandler({"http": proxy, "https": proxy}))
134
-
135
- ssl_context = _create_ssl_context(verify_ssl)
136
- if ssl_context:
137
- handlers.append(HTTPSHandler(context=ssl_context))
138
-
139
- return build_opener(*handlers)
140
-
141
-
142
- def send_http_request(method, url, body=None, headers=None, proxy=None, verify_ssl=True):
143
- # type: (str, str, str | bytes | None, dict[str, str] | None, str | None, bool | str) -> HttpResponse
32
+ from urllib2 import ( # type: ignore[no-redef]
33
+ BaseHandler,
34
+ build_opener,
35
+ HTTPBasicAuthHandler,
36
+ HTTPDefaultErrorHandler,
37
+ HTTPPasswordMgrWithDefaultRealm,
38
+ HTTPSHandler,
39
+ ProxyHandler,
40
+ Request,
41
+ )
42
+ from urllib import urlencode, quote, unquote # type: ignore[no-redef]
43
+ from httplib import HTTPSConnection # type: ignore[no-redef]
44
+
45
+ __all__ = ["request", "HttpResponse", "quote", "urlencode", "USER_AGENT"]
46
+ # Default user-agent for DDNS requests
47
+ USER_AGENT = "DDNS/{} (ddns@newfuture.cc)".format(__version__ if __version__ != "${BUILD_VERSION}" else "dev")
48
+
49
+ logger = getLogger().getChild("http")
50
+ _AUTH_URL_RE = compile(r"^(https?://)([^:/?#]+):([^@]+)@(.+)$")
51
+
52
+
53
+ def _proxy_handler(proxy):
54
+ # type: (str | None) -> ProxyHandler | None
55
+ """标准化代理格式并返回ProxyHandler对象"""
56
+ if not proxy or proxy.upper() in ("SYSTEM", "DEFAULT"):
57
+ return ProxyHandler() # 系统代理
58
+ elif proxy.upper() in ("DIRECT"):
59
+ return ProxyHandler({}) # 不使用代理
60
+ elif "://" not in proxy:
61
+ # 检查是否是 host:port 格式
62
+ logger.warning("Legacy proxy format '%s' detected, converting to 'http://%s'", proxy, proxy)
63
+ proxy = "http://" + proxy
64
+
65
+ return ProxyHandler({"http": proxy, "https": proxy})
66
+
67
+
68
+ def request(method, url, data=None, headers=None, proxies=None, verify=True, auth=None, retries=1):
69
+ # type: (str, str, str | bytes | None, dict[str, str] | None, list[str] | None, bool | str, BaseHandler | None, int) -> HttpResponse # noqa: E501
144
70
  """
145
- 发送HTTP/HTTPS请求,支持重定向跟随和灵活的SSL验证
71
+ 发送HTTP/HTTPS请求,支持自动重试和类似requests.request的参数接口
146
72
 
147
73
  Args:
148
74
  method (str): HTTP方法,如GET、POST等
149
- url (str): 请求的URL
150
- body (str | bytes | None): 请求体
151
- headers (dict[str, str] | None): 请求头
152
- proxy (str | None): 代理地址
153
- verify_ssl (bool | str): SSL验证配置
75
+ url (str): 请求的URL,支持嵌入式认证格式 https://user:pass@domain.com
76
+ data (str | bytes | None): 请求体数据
77
+ headers (dict[str, str] | None): 请求头字典
78
+ proxies (list[str | None] | None): 代理列表,支持以下格式:
79
+ - "http://host:port" - 具体代理地址
80
+ - "DIRECT" - 直连,不使用代理
81
+ - "SYSTEM" - 使用系统默认代理设置
82
+ verify (bool | str): SSL验证配置
83
+ - True: 启用标准SSL验证
84
+ - False: 禁用SSL验证
85
+ - "auto": 启用验证,失败时自动回退到不验证
86
+ - str: 自定义CA证书文件路径
87
+ auth (BaseHandler | None): 自定义认证处理器
88
+ retries (int): 最大重试次数,默认1次
154
89
 
155
90
  Returns:
156
91
  HttpResponse: 响应对象
@@ -158,40 +93,57 @@ def send_http_request(method, url, body=None, headers=None, proxy=None, verify_s
158
93
  Raises:
159
94
  URLError: 如果请求失败
160
95
  ssl.SSLError: 如果SSL验证失败
96
+ ValueError: 如果参数无效
161
97
  """
98
+ # 解析URL以检查是否包含嵌入式认证信息
99
+ m = _AUTH_URL_RE.match(url)
100
+ if m:
101
+ protocol, username, password, rest = m.groups()
102
+ url = protocol + rest
103
+ auth = HTTPBasicAuthHandler(HTTPPasswordMgrWithDefaultRealm())
104
+ auth.add_password(None, url, unquote(username), unquote(password)) # type: ignore[no-untyped-call]
105
+
162
106
  # 准备请求
163
- if isinstance(body, str):
164
- body = body.encode("utf-8")
165
-
166
- req = Request(url, data=body)
167
- req.get_method = lambda: method # type: ignore[attr-defined]
168
-
169
- if headers:
170
- for key, value in headers.items():
171
- req.add_header(key, value)
172
-
173
- # 创建opener并发送请求
174
- opener = _create_opener(proxy, verify_ssl)
175
-
176
- try:
177
- response = opener.open(req)
178
- response_headers = response.info()
179
- raw_body = response.read()
180
- decoded_body = _decode_response_body(raw_body, response_headers.get("Content-Type"))
181
- return HttpResponse(response.getcode(), getattr(response, "msg", ""), response_headers, decoded_body)
182
- except HTTPError as e:
183
- # 记录HTTP错误并读取响应体用于调试
184
- response_headers = getattr(e, "headers", {})
185
- raw_body = e.read()
186
- decoded_body = _decode_response_body(raw_body, response_headers.get("Content-Type"))
187
- logger.error("HTTP error %s: %s for %s", e.code, getattr(e, "reason", str(e)), url)
188
- return HttpResponse(e.code, getattr(e, "reason", str(e)), response_headers, decoded_body)
189
- except ssl.SSLError:
190
- if verify_ssl == "auto":
191
- logger.warning("SSL verification failed, switching to unverified connection %s", url)
192
- return send_http_request(method, url, body, headers, proxy, False)
107
+ if isinstance(data, str):
108
+ data = data.encode("utf-8")
109
+
110
+ if headers is None:
111
+ headers = {}
112
+ if not any(k.lower() == "user-agent" for k in headers.keys()):
113
+ headers["User-Agent"] = USER_AGENT # 设置默认User-Agent
114
+
115
+ handlers = [NoHTTPErrorHandler(), AutoSSLHandler(verify), RetryHandler(retries)]
116
+ handlers += [auth] if auth else []
117
+
118
+ def run(proxy_handler):
119
+ req = Request(url, data=data, headers=headers)
120
+ req.get_method = lambda: method.upper() # python 2 兼容
121
+ h = handlers + ([proxy_handler] if proxy_handler else [])
122
+ return build_opener(*h).open(req, timeout=60 if method == "GET" else 120) # 创建处理器链
123
+
124
+ if not proxies:
125
+ response = run(None) # 默认
126
+ else:
127
+ last_err = None # type: Exception # type: ignore[assignment]
128
+ for p in proxies:
129
+ logger.debug("Trying proxy: %s", p)
130
+ try:
131
+ response = run(_proxy_handler(p)) # 尝试使用代理
132
+ break # 成功后退出循环
133
+ except Exception as e:
134
+ last_err = e
193
135
  else:
194
- raise
136
+ logger.error("All proxies failed")
137
+ raise last_err # 如果所有代理都失败,抛出最后一个错误
138
+
139
+ # 处理响应
140
+ response_headers = response.info()
141
+ raw_body = response.read()
142
+ decoded_body = _decode_response_body(raw_body, response_headers.get("Content-Type"))
143
+ status_code = response.getcode()
144
+ reason = getattr(response, "msg", "")
145
+
146
+ return HttpResponse(status_code, reason, response_headers, decoded_body)
195
147
 
196
148
 
197
149
  def _decode_response_body(raw_body, content_type):
@@ -208,14 +160,13 @@ def _decode_response_body(raw_body, content_type):
208
160
  if end == -1:
209
161
  end = len(content_type)
210
162
  charset = content_type[start:end].strip("'\" ").lower()
163
+
164
+ # 处理常见别名映射
165
+ charset_aliases = {"gb2312": "gbk", "iso-8859-1": "latin-1"}
166
+ charset = charset_aliases.get(charset, charset)
167
+ if charset in charsets:
168
+ charsets.remove(charset)
211
169
  charsets.insert(0, charset)
212
- # 处理常见别名
213
- if charset == "gb2312":
214
- charsets.remove("gbk")
215
- charsets.insert(0, "gbk")
216
- elif charset == "iso-8859-1":
217
- charsets.remove("latin-1")
218
- charsets.insert(0, "latin-1")
219
170
 
220
171
  # 按优先级尝试解码
221
172
  for encoding in charsets:
@@ -224,5 +175,148 @@ def _decode_response_body(raw_body, content_type):
224
175
  except (UnicodeDecodeError, LookupError):
225
176
  continue
226
177
 
227
- # 最终后备:UTF-8替换错误字符
228
- return raw_body.decode("utf-8", errors="replace")
178
+ return raw_body.decode("utf-8", errors="replace") # 最终后备:UTF-8替换错误字符
179
+
180
+
181
+ class HttpResponse(object):
182
+ """HTTP响应封装类"""
183
+
184
+ def __init__(self, status, reason, headers, body):
185
+ # type: (int, str, Any, str) -> None
186
+ """初始化HTTP响应对象"""
187
+ self.status = status
188
+ self.reason = reason
189
+ self.headers = headers
190
+ self.body = body
191
+
192
+
193
+ class NoHTTPErrorHandler(HTTPDefaultErrorHandler): # type: ignore[misc]
194
+ """自定义HTTP错误处理器,处理所有HTTP错误状态码,返回响应而不抛出异常"""
195
+
196
+ def http_error_default(self, req, fp, code, msg, hdrs):
197
+ """处理所有HTTP错误状态码,返回响应而不抛出异常"""
198
+ logger.info("HTTP error %s: %s", code, msg)
199
+ return fp
200
+
201
+
202
+ class AutoSSLHandler(HTTPSHandler): # type: ignore[misc]
203
+ """SSL自动降级处理器,处理 unable to get local issuer certificate 错误"""
204
+
205
+ _ssl_cache = {} # type: dict[str, ssl.SSLContext|None]
206
+
207
+ def __init__(self, verify):
208
+ # type: (bool | str) -> None
209
+ self._verify = verify
210
+ self._context = self._ssl_context()
211
+ # 兼容性:优先使用context参数,失败时降级
212
+ try: # python 3 / python 2.7.9+
213
+ HTTPSHandler.__init__(self, context=self._context)
214
+ except (TypeError, AttributeError): # python 2.7.8-
215
+ HTTPSHandler.__init__(self)
216
+
217
+ def https_open(self, req):
218
+ """处理HTTPS请求,自动处理SSL错误"""
219
+ try:
220
+ return self._open(req)
221
+ except OSError as e: # SSL auto模式:处理本地证书错误
222
+ ssl_errors = ("unable to get local issuer certificate", "Basic Constraints of CA cert not marked critical")
223
+ if self._verify == "auto" and any(err in str(e) for err in ssl_errors):
224
+ logger.warning("SSL error (%s), switching to unverified connection", str(e))
225
+ self._verify = False # 不验证SSL
226
+ self._context = self._ssl_context() # 确保上下文已更新
227
+ return self._open(req) # 重试请求
228
+ else:
229
+ logger.debug("error: (%s)", e)
230
+ raise
231
+
232
+ def _open(self, req):
233
+ try: # python 3
234
+ return self.do_open(HTTPSConnection, req, context=self._context)
235
+ except (TypeError, AttributeError): # python 2.7.6- Fallback for older Python versions
236
+ logger.info("Falling back to parent https_open method for compatibility")
237
+ return HTTPSHandler.https_open(self, req)
238
+
239
+ def _ssl_context(self):
240
+ # type: () -> ssl.SSLContext | None
241
+ """创建或获取缓存的SSLContext"""
242
+ cache_key = "default" # 缓存键
243
+ if not self._verify:
244
+ cache_key = "unverified"
245
+ if cache_key not in self._ssl_cache:
246
+ self._ssl_cache[cache_key] = (
247
+ ssl._create_unverified_context() if hasattr(ssl, "_create_unverified_context") else None
248
+ )
249
+ elif hasattr(self._verify, "lower") and self._verify.lower() not in ("auto", "true"): # type: ignore
250
+ cache_key = str(self._verify)
251
+ if cache_key not in self._ssl_cache:
252
+ self._ssl_cache[cache_key] = ssl.create_default_context(cafile=cache_key)
253
+ elif cache_key not in self._ssl_cache:
254
+ self._ssl_cache[cache_key] = ssl.create_default_context()
255
+ if not self._ssl_cache[cache_key].get_ca_certs(): # type: ignore
256
+ logger.info("No system CA certificates found, loading default CA certificates")
257
+ self._load_system_ca_certs(self._ssl_cache[cache_key]) # type: ignore
258
+
259
+ return self._ssl_cache[cache_key]
260
+
261
+ def _load_system_ca_certs(self, ssl_context):
262
+ # type: (ssl.SSLContext) -> None
263
+ """加载系统CA证书"""
264
+ ca_paths = [
265
+ # Linux/Unix常用路径
266
+ "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
267
+ "/etc/pki/tls/certs/ca-bundle.crt", # RedHat/CentOS
268
+ "/etc/ssl/ca-bundle.pem", # OpenSUSE
269
+ "/etc/ssl/cert.pem", # OpenBSD
270
+ "/usr/local/share/certs/ca-root-nss.crt", # FreeBSD
271
+ "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", # Fedora/RHEL
272
+ # macOS路径
273
+ "/usr/local/etc/openssl/cert.pem", # macOS with Homebrew
274
+ "/opt/local/etc/openssl/cert.pem", # macOS with MacPorts
275
+ ]
276
+
277
+ for ca_path in ca_paths:
278
+ if os.path.isfile(ca_path):
279
+ try:
280
+ ssl_context.load_verify_locations(ca_path)
281
+ logger.info("Loaded CA certificates from: %s", ca_path)
282
+ return # 成功加载后立即返回
283
+ except Exception as e:
284
+ logger.warning("Failed to load CA certificates from %s: %s", ca_path, e)
285
+
286
+
287
+ class RetryHandler(BaseHandler): # type: ignore[misc]
288
+ """HTTP重试处理器,自动重试指定状态码和网络错误"""
289
+
290
+ handler_order = 100
291
+ RETRY_CODES = (408, 429, 500, 502, 503, 504)
292
+
293
+ def __init__(self, retries=3):
294
+ # type: (int) -> None
295
+ """初始化重试处理器"""
296
+ self._in_retry = False # 防止递归调用的标志
297
+ self.retries = retries # 始终设置retries属性
298
+ if retries > 0:
299
+ self.default_open = self._open
300
+
301
+ def _open(self, req):
302
+ """实际的重试逻辑,处理所有协议"""
303
+ if self._in_retry:
304
+ return None # 防止递归调用
305
+
306
+ self._in_retry = True
307
+
308
+ try:
309
+ for attempt in range(1, self.retries + 1):
310
+ try:
311
+ res = self.parent.open(req, timeout=req.timeout)
312
+ if not hasattr(res, "getcode") or res.getcode() not in self.RETRY_CODES:
313
+ return res # 成功响应直接返回
314
+ logger.warning("HTTP %d error, retrying in %d seconds", res.getcode(), 2**attempt)
315
+ except (socket.timeout, socket.gaierror, socket.herror) as e:
316
+ logger.warning("Request failed, retrying in %d seconds: %s", 2**attempt, str(e))
317
+
318
+ time.sleep(2**attempt)
319
+ continue
320
+ return self.parent.open(req, timeout=req.timeout) # 最后一次尝试
321
+ finally:
322
+ self._in_retry = False