fix(download): validate torrent bytes before html fallback

This commit is contained in:
jxxghp
2026-09-14 00:32:11 +08:00
parent 11a2a09bf6
commit a51acb05e5
2 changed files with 101 additions and 15 deletions

View File

@@ -2,7 +2,7 @@ import datetime
import re
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, List, Optional, Protocol, Tuple, Union
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union
from urllib.parse import unquote
from torrentool.api import Torrent
@@ -119,6 +119,19 @@ def _parse_filter_size_range(size_range: str) -> Tuple[str, float, Optional[floa
return "unknown", 0, None
def _cache_valid_torrent_content(
parser: Callable[[Union[str, bytes]], Tuple[str, List[str]]],
cache_backend: Any,
cache_path: Path,
content: bytes,
) -> Tuple[str, List[str]]:
"""解析合法种子并缓存原始二进制内容。"""
folder_name, file_list = parser(content)
if file_list:
cache_backend.set(cache_path.as_posix(), content, region="torrents")
return folder_name, file_list
class TorrentHelper:
"""
种子帮助类
@@ -187,6 +200,13 @@ class TorrentHelper:
if req.content.startswith(b"magnet:"):
# 磁力链接
return cache_path, req.text, "", [], "获取到磁力链接"
# 优先按原始二进制解析,避免合法 torrent 被误判为首次下载 HTML。
folder_name, file_list = _cache_valid_torrent_content(
self.get_fileinfo_from_torrent_content, cache_backend, cache_path, req.content
)
if file_list:
# 成功拿到种子数据
return cache_path, req.content, folder_name, file_list, ""
if "下载种子文件".encode("utf-8") in req.content:
# 首次下载提示页面
skip_flag = False
@@ -224,24 +244,17 @@ class TorrentHelper:
logger.warn(f"触发了站点首次种子下载,尝试自动跳过时出现错误:{str(err)}")
if not skip_flag:
return cache_path, None, "", [], "种子数据有误,请确认链接是否正确,如为PT站点则需手工在站点下载一次种子"
# 首次下载提示页面完成后,重新按原始二进制解析 POST 响应。
assert req is not None
# 种子内容
if req.content:
# 检查是不是种子文件,如果不是仍然抛出异常
try:
# 获取种子目录和文件清单
folder_name, file_list = self.get_fileinfo_from_torrent_content(req.content)
if file_list:
# 保存到缓存
cache_backend.set(cache_path.as_posix(), req.content, region="torrents")
folder_name, file_list = _cache_valid_torrent_content(
self.get_fileinfo_from_torrent_content, cache_backend, cache_path, req.content
)
if file_list:
# 成功拿到种子数据
return cache_path, req.content, folder_name, file_list, ""
except Exception as err:
logger.error(f"种子文件解析失败:{str(err)}")
# 种子数据仍然错误
return cache_path, None, "", [], "种子数据有误,请确认链接是否正确"
# 返回失败
return cache_path, None, "", [], ""
# 解析失败时终止,避免把 HTML 继续下沉到下载器或缓存。
return cache_path, None, "", [], "种子数据有误,请确认链接是否正确"
elif req is None:
return cache_path, None, "", [], "无法打开链接"
elif req.status_code == 429:

View File

@@ -125,6 +125,79 @@ def test_torrent_fake_port_preserves_rate_limit_message(monkeypatch) -> None:
assert result[1:] == (None, "", [], "触发站点流控,请稍后重试")
def test_torrent_binary_is_parsed_before_first_download_html_detection(monkeypatch) -> None:
"""合法二进制种子包含首次下载提示文本时仍应按 torrent 解析。"""
marker = "下载种子文件".encode("utf-8")
torrent_content = (
b"d4:infod7:comment"
+ str(len(marker)).encode("ascii")
+ b":"
+ marker
+ b"4:name7:foo.mkv6:lengthi1eee"
)
cached: list[bytes] = []
class Cache:
"""记录合法种子缓存写入,不接触真实缓存后端。"""
@staticmethod
def get(*_args, **_kwargs):
"""固定返回缓存未命中。"""
return None
@staticmethod
def set(_key, value, **_kwargs):
"""记录原始二进制种子内容。"""
cached.append(value)
class HttpPort:
"""返回包含提示文本的合法二进制种子响应。"""
@staticmethod
def request(**_kwargs):
"""返回固定 torrent 响应。"""
return SimpleNamespace(
status_code=200,
headers={"Content-Type": "application/x-bittorrent"},
content=torrent_content,
text=torrent_content.decode("utf-8"),
reason="",
)
monkeypatch.setattr(torrent_module, "FileCache", Cache)
configure_torrent_port(HttpPort())
result = TorrentHelper().download_torrent("https://example.test/download/1")
assert result[1:] == (torrent_content, "", ["foo.mkv"], "")
assert cached == [torrent_content]
def test_torrent_invalid_http_200_body_is_rejected_before_downloader(monkeypatch) -> None:
"""HTTP 200 返回非种子内容时应在下载层终止并避免缓存。"""
class HttpPort:
"""返回登录页面的种子 HTTP 假端口。"""
@staticmethod
def request(**_kwargs):
"""返回固定 HTML 响应。"""
return SimpleNamespace(
status_code=200,
headers={"Content-Type": "text/html"},
content=b"<html>login</html>",
text="<html>login</html>",
reason="",
)
monkeypatch.setattr(torrent_module, "FileCache", _EmptyCache)
configure_torrent_port(HttpPort())
result = TorrentHelper().download_torrent("https://example.test/download/2")
assert result[1:] == (None, "", [], "种子数据有误,请确认链接是否正确")
def test_cookie_invalid_parameters_keep_legacy_result_without_ports() -> None:
"""参数错误属于 Application 语义,不应依赖浏览器端口已装配。"""
assert CookieHelper().get_site_cookie_ua("", "", "") == (