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.

ddns/util/cache.py CHANGED
@@ -4,30 +4,25 @@ cache module
4
4
  文件缓存
5
5
  """
6
6
 
7
-
8
7
  from os import path, stat
9
8
  from pickle import dump, load
10
9
  from time import time
10
+ from logging import getLogger, Logger # noqa: F401
11
11
 
12
- from logging import info, debug, warning
13
-
14
- try: # python 3
15
- from collections.abc import MutableMapping
16
- except ImportError: # python 2
17
- from collections import MutableMapping
18
12
 
19
-
20
- class Cache(MutableMapping):
13
+ class Cache(dict):
21
14
  """
22
15
  using file to Cache data as dictionary
23
16
  """
24
17
 
25
- def __init__(self, path, sync=False):
26
- self.__data = {}
18
+ def __init__(self, path, logger=None, sync=False):
19
+ # type: (str, Logger | None, bool) -> None
20
+ super(Cache, self).__init__()
27
21
  self.__filename = path
28
22
  self.__sync = sync
29
23
  self.__time = time()
30
24
  self.__changed = False
25
+ self.__logger = (logger or getLogger()).getChild("Cache")
31
26
  self.load()
32
27
 
33
28
  @property
@@ -35,7 +30,7 @@ class Cache(MutableMapping):
35
30
  """
36
31
  缓存修改时间
37
32
  """
38
- return self.__time
33
+ return self.__time or 0
39
34
 
40
35
  def load(self, file=None):
41
36
  """
@@ -44,44 +39,35 @@ class Cache(MutableMapping):
44
39
  if not file:
45
40
  file = self.__filename
46
41
 
47
- debug('load cache data from %s', file)
48
- if path.isfile(file):
49
- with open(self.__filename, 'rb') as data:
42
+ self.__logger.debug("load cache data from %s", file)
43
+ if file and path.isfile(file):
44
+ with open(file, "rb") as data:
50
45
  try:
51
- self.__data = load(data)
46
+ loaded_data = load(data)
47
+ self.clear()
48
+ self.update(loaded_data)
52
49
  self.__time = stat(file).st_mtime
53
50
  return self
54
51
  except ValueError:
55
52
  pass
56
53
  except Exception as e:
57
- warning(e)
54
+ self.__logger.warning(e)
58
55
  else:
59
- info('cache file not exist')
56
+ self.__logger.info("cache file not exist")
60
57
 
61
- self.__data = {}
58
+ self.clear()
62
59
  self.__time = time()
63
60
  self.__changed = True
64
61
  return self
65
62
 
66
- def data(self, key=None, default=None):
67
- """
68
- 获取当前字典或者制定得键值
69
- """
70
- if self.__sync:
71
- self.load()
72
-
73
- if key is None:
74
- return self.__data
75
- else:
76
- return self.__data.get(key, default)
77
-
78
63
  def sync(self):
79
- """Sync the write buffer with the cache files and clear the buffer.
80
- """
81
- if self.__changed:
82
- with open(self.__filename, 'wb') as data:
83
- dump(self.__data, data)
84
- debug('save cache data to %s', self.__filename)
64
+ """Sync the write buffer with the cache files and clear the buffer."""
65
+ if self.__changed and self.__filename:
66
+ with open(self.__filename, "wb") as data:
67
+ # 只保存非私有字段(不以__开头的字段)
68
+ filtered_data = {k: v for k, v in super(Cache, self).items() if not k.startswith("__")}
69
+ dump(filtered_data, data)
70
+ self.__logger.debug("save cache data to %s", self.__filename)
85
71
  self.__time = time()
86
72
  self.__changed = False
87
73
  return self
@@ -91,10 +77,10 @@ class Cache(MutableMapping):
91
77
  If a closed :class:`FileCache` object's methods are called, a
92
78
  :exc:`ValueError` will be raised.
93
79
  """
94
- self.sync()
95
- del self.__data
96
- del self.__filename
97
- del self.__time
80
+ if self.__filename:
81
+ self.sync()
82
+ self.__filename = None
83
+ self.__time = None
98
84
  self.__sync = False
99
85
 
100
86
  def __update(self):
@@ -105,35 +91,62 @@ class Cache(MutableMapping):
105
91
  self.__time = time()
106
92
 
107
93
  def clear(self):
108
- if self.data() is not None:
109
- self.__data = {}
94
+ # 只清除非私有字段(不以__开头的字段)
95
+ keys_to_remove = [key for key in super(Cache, self).keys() if not key.startswith("__")]
96
+ if keys_to_remove:
97
+ for key in keys_to_remove:
98
+ super(Cache, self).__delitem__(key)
110
99
  self.__update()
111
100
 
101
+ def get(self, key, default=None):
102
+ """
103
+ 获取指定键的值,如果键不存在则返回默认值
104
+ :param key: 键
105
+ :param default: 默认值
106
+ :return: 键对应的值或默认值
107
+ """
108
+ if key is None and default is None:
109
+ return {k: v for k, v in super(Cache, self).items() if not k.startswith("__")}
110
+ return super(Cache, self).get(key, default)
111
+
112
112
  def __setitem__(self, key, value):
113
- if self.data(key) != value:
114
- self.__data[key] = value
115
- self.__update()
113
+ if self.get(key) != value:
114
+ super(Cache, self).__setitem__(key, value)
115
+ # 私有字段(以__开头)不触发同步
116
+ if not key.startswith("__"):
117
+ self.__update()
116
118
 
117
119
  def __delitem__(self, key):
118
- if key in self.data():
119
- del self.__data[key]
120
+ # 检查键是否存在,如果不存在则直接返回,不抛错
121
+ if not super(Cache, self).__contains__(key):
122
+ return
123
+ super(Cache, self).__delitem__(key)
124
+ # 私有字段(以__开头)不触发同步
125
+ if not key.startswith("__"):
120
126
  self.__update()
121
127
 
122
128
  def __getitem__(self, key):
123
- return self.data(key)
129
+ return super(Cache, self).__getitem__(key)
124
130
 
125
131
  def __iter__(self):
126
- for key in self.data():
127
- yield key
132
+ # 只迭代非私有字段(不以__开头的字段)
133
+ for key in super(Cache, self).__iter__():
134
+ if not key.startswith("__"):
135
+ yield key
136
+
137
+ def __items__(self):
138
+ # 只返回非私有字段(不以__开头的字段)
139
+ return ((key, value) for key, value in super(Cache, self).items() if not key.startswith("__"))
128
140
 
129
141
  def __len__(self):
130
- return len(self.data())
142
+ # 不计算以__开头的私有字段
143
+ return len([key for key in super(Cache, self).keys() if not key.startswith("__")])
131
144
 
132
145
  def __contains__(self, key):
133
- return key in self.data()
146
+ return super(Cache, self).__contains__(key)
134
147
 
135
148
  def __str__(self):
136
- return self.data().__str__()
149
+ return super(Cache, self).__str__()
137
150
 
138
151
  def __del__(self):
139
152
  self.close()
ddns/util/config.py CHANGED
@@ -54,7 +54,7 @@ def parse_array_string(value, enable_simple_split):
54
54
  仅当 trim 之后以 '[' 开头以 ']' 结尾时,才尝试使用 ast.literal_eval 解析
55
55
  默认返回原始字符串
56
56
  """
57
- if not isinstance(value, str):
57
+ if not hasattr(value, "strip"): # 非字符串
58
58
  return value
59
59
 
60
60
  trimmed = value.strip()
@@ -71,10 +71,10 @@ def parse_array_string(value, enable_simple_split):
71
71
  elif enable_simple_split:
72
72
  # 尝试使用逗号或分号分隔符解析
73
73
  sep = None
74
- if ',' in trimmed:
75
- sep = ','
76
- elif ';' in trimmed:
77
- sep = ';'
74
+ if "," in trimmed:
75
+ sep = ","
76
+ elif ";" in trimmed:
77
+ sep = ";"
78
78
  if sep:
79
79
  return [item.strip() for item in trimmed.split(sep) if item.strip()]
80
80
  return value
@@ -84,9 +84,8 @@ def get_system_info_str():
84
84
  system = platform.system()
85
85
  release = platform.release()
86
86
  machine = platform.machine()
87
- arch = platform.architecture()[0] # '64bit' or '32bit'
88
-
89
- return "{}-{} {} ({})".format(system, release, machine, arch)
87
+ arch = platform.architecture()
88
+ return "{}-{} {} {}".format(system, release, machine, arch)
90
89
 
91
90
 
92
91
  def get_python_info_str():
@@ -100,21 +99,13 @@ def init_config(description, doc, version, date):
100
99
  配置
101
100
  """
102
101
  global __cli_args
103
- parser = ArgumentParser(
104
- description=description, epilog=doc, formatter_class=RawTextHelpFormatter
105
- )
102
+ parser = ArgumentParser(description=description, epilog=doc, formatter_class=RawTextHelpFormatter)
106
103
  sysinfo = get_system_info_str()
107
104
  pyinfo = get_python_info_str()
108
105
  version_str = "v{} ({})\n{}\n{}".format(version, date, pyinfo, sysinfo)
109
106
  parser.add_argument("-v", "--version", action="version", version=version_str)
110
- parser.add_argument(
111
- "-c", "--config", metavar="FILE", help="load config file [配置文件路径]"
112
- )
113
- parser.add_argument(
114
- "--debug",
115
- action="store_true",
116
- help="debug mode [调试模式等效 --log.level=DEBUG]",
117
- )
107
+ parser.add_argument("-c", "--config", metavar="FILE", help="load config file [配置文件路径]")
108
+ parser.add_argument("--debug", action="store_true", help="debug mode [开启调试模式]")
118
109
 
119
110
  # 参数定义
120
111
  parser.add_argument(
@@ -129,6 +120,7 @@ def init_config(description, doc, version, date):
129
120
  "he",
130
121
  "huaweidns",
131
122
  "callback",
123
+ "debug",
132
124
  ],
133
125
  )
134
126
  parser.add_argument("--id", help="API ID or email [对应账号ID或邮箱]")
@@ -162,6 +154,7 @@ def init_config(description, doc, version, date):
162
154
  help="IPv6 domains [IPv6域名列表, 可配置多个域名]",
163
155
  )
164
156
  parser.add_argument("--ttl", type=int, help="DNS TTL(s) [设置域名解析过期时间]")
157
+ parser.add_argument("--line", help="DNS line/route [DNS线路设置,如电信、联通、移动等]")
165
158
  parser.add_argument(
166
159
  "--proxy",
167
160
  nargs="*",
@@ -183,23 +176,25 @@ def init_config(description, doc, version, date):
183
176
  help="disable cache [关闭缓存等效 --cache=false]",
184
177
  )
185
178
  parser.add_argument(
186
- "--log.file", metavar="FILE", help="log file [日志文件,默认标准输出]"
179
+ "--ssl",
180
+ help="SSL certificate verification [SSL证书验证方式]: "
181
+ "true(强制验证), false(禁用验证), auto(自动降级), /path/to/cert.pem(自定义证书)",
187
182
  )
183
+ parser.add_argument("--log.file", metavar="FILE", help="log file [日志文件,默认标准输出]")
188
184
  parser.add_argument("--log.level", type=log_level, metavar="|".join(log_levels))
189
- parser.add_argument(
190
- "--log.format", metavar="FORMAT", help="log format [设置日志打印格式]"
191
- )
192
- parser.add_argument(
193
- "--log.datefmt", metavar="FORMAT", help="date format [日志时间打印格式]"
194
- )
185
+ parser.add_argument("--log.format", metavar="FORMAT", help="log format [设置日志打印格式]")
186
+ parser.add_argument("--log.datefmt", metavar="FORMAT", help="date format [日志时间打印格式]")
195
187
 
196
188
  __cli_args = parser.parse_args()
197
- if __cli_args.debug:
198
- # 如果启用调试模式,则设置日志级别为 DEBUG
189
+ is_debug = getattr(__cli_args, "debug", False)
190
+ if is_debug:
191
+ # 如果启用调试模式,则强制设置日志级别为 DEBUG
199
192
  setattr(__cli_args, "log.level", log_level("DEBUG"))
193
+ if not hasattr(__cli_args, "cache"):
194
+ setattr(__cli_args, "cache", False) # 禁用缓存
200
195
 
201
- is_configfile_required = not get_config("token") and not get_config("id")
202
- config_file = get_config("config")
196
+ config_required = not get_config("token") and not get_config("id")
197
+ config_file = get_config("config") # type: str | None # type: ignore
203
198
  if not config_file:
204
199
  # 未指定配置文件且需要读取文件时,依次查找
205
200
  cfgs = [
@@ -212,7 +207,7 @@ def init_config(description, doc, version, date):
212
207
  if path.isfile(config_file):
213
208
  __load_config(config_file)
214
209
  __cli_args.config = config_file
215
- elif is_configfile_required:
210
+ elif config_required:
216
211
  error("Config file is required, but not found: %s", config_file)
217
212
  # 如果需要配置文件但没有指定,则自动生成
218
213
  if generate_config(config_file):
@@ -296,13 +291,15 @@ def generate_config(config_path):
296
291
  "$schema": "https://ddns.newfuture.cc/schema/v4.0.json",
297
292
  "id": "YOUR ID or EMAIL for DNS Provider",
298
293
  "token": "YOUR TOKEN or KEY for DNS Provider",
299
- "dns": "dnspod",
294
+ "dns": "debug", # DNS Provider, default is print
300
295
  "ipv4": ["newfuture.cc", "ddns.newfuture.cc"],
301
296
  "ipv6": ["newfuture.cc", "ipv6.ddns.newfuture.cc"],
302
297
  "index4": "default",
303
298
  "index6": "default",
304
299
  "ttl": None,
300
+ "line": None,
305
301
  "proxy": None,
302
+ "ssl": "auto",
306
303
  "log": {"level": "INFO"},
307
304
  }
308
305
  try:
ddns/util/http.py ADDED
@@ -0,0 +1,277 @@
1
+ # coding=utf-8
2
+ """
3
+ HTTP请求工具模块
4
+
5
+ HTTP utilities module for DDNS project.
6
+ Provides common HTTP functionality including redirect following support.
7
+
8
+ @author: NewFuture
9
+ """
10
+
11
+ from logging import getLogger
12
+ import ssl
13
+ import os
14
+
15
+ try: # python 3
16
+ from http.client import HTTPSConnection, HTTPConnection, HTTPException
17
+ from urllib.parse import urlparse
18
+ except ImportError: # python 2
19
+ from httplib import HTTPSConnection, HTTPConnection, HTTPException # type: ignore[no-redef]
20
+ from urlparse import urlparse # type: ignore[no-redef]
21
+
22
+ __all__ = ["send_http_request", "HttpResponse"]
23
+
24
+ logger = getLogger().getChild(__name__)
25
+
26
+
27
+ class HttpResponse(object):
28
+ """HTTP响应封装类"""
29
+
30
+ def __init__(self, status, reason, headers, body):
31
+ # type: (int, str, list[tuple[str, str]], str) -> None
32
+ """
33
+ 初始化HTTP响应对象
34
+
35
+ Args:
36
+ status (int): HTTP状态码
37
+ reason (str): 状态原因短语
38
+ headers (list[tuple[str, str]]): 响应头列表,保持原始格式和顺序
39
+ body (str): 响应体内容
40
+ """
41
+ self.status = status
42
+ self.reason = reason
43
+ self.headers = headers
44
+ self.body = body
45
+
46
+ def get_header(self, name, default=None):
47
+ # type: (str, str | None) -> str | None
48
+ """
49
+ 获取指定名称的头部值(不区分大小写)
50
+
51
+ Args:
52
+ name (str): 头部名称
53
+
54
+ Returns:
55
+ str | None: 头部值,如果不存在则返回None
56
+ """
57
+ name_lower = name.lower()
58
+ for header_name, header_value in self.headers:
59
+ if header_name.lower() == name_lower:
60
+ return header_value
61
+ return default
62
+
63
+
64
+ def _create_connection(hostname, port, is_https, proxy, verify_ssl):
65
+ # type: (str, int | None, bool, str | None, bool | str) -> HTTPConnection | HTTPSConnection
66
+ """创建HTTP/HTTPS连接"""
67
+ target = proxy or hostname
68
+
69
+ if not is_https:
70
+ conn = HTTPConnection(target, port)
71
+ else:
72
+ ssl_context = ssl.create_default_context()
73
+
74
+ if verify_ssl is False:
75
+ # 禁用SSL验证
76
+ ssl_context.check_hostname = False
77
+ ssl_context.verify_mode = ssl.CERT_NONE
78
+ elif hasattr(verify_ssl, "lower") and verify_ssl.lower() not in ("auto", "true"): # type: ignore[union-attr]
79
+ # 使用自定义CA证书 lower 判断 str/unicode 兼容 python2
80
+ try:
81
+ ssl_context.load_verify_locations(verify_ssl) # type: ignore[arg-type]
82
+ except Exception as e:
83
+ logger.error("Failed to load CA certificate from %s: %s", verify_ssl, e)
84
+ else:
85
+ # 默认验证,尝试加载系统证书
86
+ _load_system_ca_certs(ssl_context)
87
+ conn = HTTPSConnection(target, port, context=ssl_context)
88
+
89
+ # 设置代理隧道
90
+ if proxy:
91
+ conn.set_tunnel(hostname, port) # type: ignore[attr-defined]
92
+
93
+ return conn
94
+
95
+
96
+ def _load_system_ca_certs(ssl_context):
97
+ # type: (ssl.SSLContext) -> None
98
+ """加载系统CA证书"""
99
+ # 常见CA证书路径
100
+ ca_paths = [
101
+ # Linux/Unix常用路径
102
+ "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
103
+ "/etc/pki/tls/certs/ca-bundle.crt", # RedHat/CentOS
104
+ "/etc/ssl/ca-bundle.pem", # OpenSUSE
105
+ "/etc/ssl/cert.pem", # OpenBSD
106
+ "/usr/local/share/certs/ca-root-nss.crt", # FreeBSD
107
+ "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", # Fedora/RHEL
108
+ # macOS路径
109
+ "/usr/local/etc/openssl/cert.pem", # macOS with Homebrew
110
+ "/opt/local/etc/openssl/cert.pem", # macOS with MacPorts
111
+ ]
112
+
113
+ # Windows额外路径
114
+ if os.name == "nt":
115
+ ca_paths.append("C:\\Program Files\\Git\\mingw64\\ssl\\cert.pem")
116
+ ca_paths.append("C:\\Program Files\\OpenSSL\\ssl\\cert.pem")
117
+
118
+ loaded_count = 0
119
+ for ca_path in ca_paths:
120
+ if os.path.isfile(ca_path):
121
+ try:
122
+ ssl_context.load_verify_locations(ca_path)
123
+ loaded_count += 1
124
+ logger.debug("Loaded CA certificates from: %s", ca_path)
125
+ except Exception as e:
126
+ logger.debug("Failed to load CA certificates from %s: %s", ca_path, e)
127
+
128
+ if loaded_count > 0:
129
+ logger.debug("Successfully loaded CA certificates from %d locations", loaded_count)
130
+
131
+
132
+ def _close_connection(conn):
133
+ # type: (HTTPConnection | HTTPSConnection) -> None
134
+ """关闭HTTP/HTTPS连接"""
135
+ try:
136
+ conn.close()
137
+ except Exception as e:
138
+ logger.warning("Failed to close connection: %s", e)
139
+
140
+
141
+ def send_http_request(method, url, body=None, headers=None, proxy=None, max_redirects=5, verify_ssl=True):
142
+ # type: (str, str, str | bytes | None, dict[str, str] | None, str | None, int, bool | str) -> HttpResponse
143
+ """
144
+ 发送HTTP/HTTPS请求,支持重定向跟随和灵活的SSL验证
145
+ Send HTTP/HTTPS request with support for redirect following and flexible SSL verification.
146
+ Args:
147
+ method (str): HTTP方法,如GET、POST等
148
+ url (str): 请求的URL
149
+ body (str | bytes | None): 请求体
150
+ headers (dict[str, str] | None): 请求头
151
+ proxy (str | None): 代理地址
152
+ max_redirects (int): 最大重定向次数
153
+ verify_ssl (bool | str): 是否验证SSL证书
154
+ Returns:
155
+ HttpResponse: 响应对象,包含状态码、头部和解码后的内容
156
+ Raises:
157
+ HTTPException: 如果请求失败或重定向次数超过限制
158
+ ssl.SSLError: 如果SSL验证失败
159
+ """
160
+ if max_redirects <= 0:
161
+ raise HTTPException("Too many redirects")
162
+
163
+ # 解析URL
164
+ url_obj = urlparse(url)
165
+ is_https = url_obj.scheme == "https"
166
+ hostname = url_obj.hostname or url_obj.netloc.split(":")[0]
167
+ request_path = "{}?{}".format(url_obj.path, url_obj.query) if url_obj.query else url_obj.path
168
+ headers = headers or {}
169
+
170
+ # 创建连接
171
+ actual_verify_ssl = verify_ssl
172
+ conn = _create_connection(hostname, url_obj.port, is_https, proxy, verify_ssl)
173
+
174
+ # 执行请求,处理SSL错误
175
+ try:
176
+ conn.request(method, request_path, body, headers)
177
+ response = conn.getresponse()
178
+ except ssl.SSLError:
179
+ _close_connection(conn)
180
+ if verify_ssl == "auto" and is_https:
181
+ logger.warning("SSL verification failed, switching to unverified connection %s", url)
182
+ # 重新连接,忽略SSL验证
183
+ conn = _create_connection(hostname, url_obj.port, is_https, proxy, False)
184
+ conn.request(method, request_path, body, headers)
185
+ response = conn.getresponse()
186
+ actual_verify_ssl = False
187
+ else:
188
+ raise
189
+
190
+ # 检查重定向
191
+ status = response.status
192
+ if 300 <= status < 400:
193
+ location = response.getheader("Location")
194
+ _close_connection(conn)
195
+ if not location:
196
+ # 无Location头的重定向
197
+ logger.warning("Redirect status %d but no Location header", status)
198
+ location = ""
199
+
200
+ # 构建重定向URL
201
+ redirect_url = _build_redirect_url(location, "{}://{}".format(url_obj.scheme, url_obj.netloc), url_obj.path)
202
+
203
+ # 如果重定向URL没有查询字符串,但原始URL有,则附加
204
+ if url_obj.query and "?" not in redirect_url:
205
+ redirect_url += "?" + url_obj.query
206
+
207
+ # 确定重定向方法:303或302+POST转为GET,其他保持原方法
208
+ if status == 303 or (status == 302 and method == "POST"):
209
+ method, body = "GET", None
210
+ # 如果从POST转为GET,移除相关的头部
211
+ if headers:
212
+ headers = {k: v for k, v in headers.items() if k.lower() not in ("content-length", "content-type")}
213
+
214
+ logger.info("Redirecting [%d] to: %s", status, redirect_url)
215
+ # 递归处理重定向
216
+ return send_http_request(method, redirect_url, body, headers, proxy, max_redirects - 1, actual_verify_ssl)
217
+
218
+ # 处理最终响应
219
+ content_type = response.getheader("Content-Type")
220
+ response_headers = response.getheaders()
221
+ raw_body = response.read()
222
+ _close_connection(conn)
223
+
224
+ # 解码响应体并创建响应对象
225
+ decoded_body = _decode_response_body(raw_body, content_type)
226
+ return HttpResponse(status, response.reason, response_headers, decoded_body)
227
+
228
+
229
+ def _build_redirect_url(location, base, path):
230
+ # type: (str, str, str) -> str
231
+ """构建重定向URL,使用简单的字符串操作"""
232
+ if location.startswith("http"):
233
+ return location
234
+
235
+ if location.startswith("/"):
236
+ # 绝对路径:使用base的scheme和netloc
237
+ base_url = urlparse(base)
238
+ return "{}://{}{}".format(base_url.scheme, base_url.netloc, location)
239
+ else:
240
+ base_path = path.rsplit("/", 1)[0] if "/" in path else ""
241
+ if not base_path.endswith("/"):
242
+ base_path += "/"
243
+ return base + base_path + location
244
+
245
+
246
+ def _decode_response_body(raw_body, content_type):
247
+ # type: (bytes, str | None) -> str
248
+ """解码HTTP响应体,优先使用UTF-8"""
249
+ if not raw_body:
250
+ return ""
251
+
252
+ # 从Content-Type提取charset
253
+ charsets = ["utf-8", "gbk", "ascii", "latin-1"]
254
+ if content_type and "charset=" in content_type.lower():
255
+ start = content_type.lower().find("charset=") + 8
256
+ end = content_type.find(";", start)
257
+ if end == -1:
258
+ end = len(content_type)
259
+ charset = content_type[start:end].strip("'\" ").lower()
260
+ charsets.insert(0, charset)
261
+ # 处理常见别名
262
+ if charset == "gb2312":
263
+ charsets.remove("gbk")
264
+ charsets.insert(0, "gbk")
265
+ elif charset == "iso-8859-1":
266
+ charsets.remove("latin-1")
267
+ charsets.insert(0, "latin-1")
268
+
269
+ # 按优先级尝试解码
270
+ for encoding in charsets:
271
+ try:
272
+ return raw_body.decode(encoding)
273
+ except (UnicodeDecodeError, LookupError):
274
+ continue
275
+
276
+ # 最终后备:UTF-8替换错误字符
277
+ return raw_body.decode("utf-8", errors="replace")