返回 JoyAI-Echo
http_download.py
1 """Safe HTTP download helper — no redirects, size cap, status 200 only."""
2
3 from __future__ import annotations
4
5 import urllib.error
6 import urllib.request
7 from dataclasses import dataclass
8
9 _DEFAULT_MAX_BYTES = 100 * 1024 * 1024
10 _DEFAULT_TIMEOUT_S = 60.0
11
12
13 @dataclass(frozen=True)
14 class HttpDownloadResult:
15 data: bytes
16 content_type: str
17 content_length: int | None
18
19
20 class HttpDownloadError(RuntimeError):
21 """Raised when an HTTP download fails or exceeds policy limits."""
22
23
24 class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
25 def redirect_request(self, req, fp, code, msg, headers, newurl):
26 raise HttpDownloadError("下载失败:禁止重定向")
27
28
29 def download_http_bytes(
30 url: str,
31 *,
32 max_bytes: int = _DEFAULT_MAX_BYTES,
33 timeout_s: float = _DEFAULT_TIMEOUT_S,
34 ) -> HttpDownloadResult:
35 """Download *url* into memory with SSRF follow-up protections.
36
37 - Does not follow redirects (302 → internal IP bypass).
38 - Accepts HTTP 200 only.
39 - Caps response body at *max_bytes* (+1 byte probe for oversize detection).
40 """
41 if max_bytes <= 0:
42 raise HttpDownloadError("无效的大小限制")
43
44 opener = urllib.request.build_opener(_NoRedirectHandler)
45 request = urllib.request.Request(url, method="GET")
46 request.add_header("Accept", "*/*")
47
48 try:
49 with opener.open(request, timeout=timeout_s) as response:
50 status = getattr(response, "status", None) or response.getcode()
51 if status != 200:
52 raise HttpDownloadError(f"下载失败,HTTP状态码: {status}")
53
54 content_type = response.headers.get("Content-Type") or "application/octet-stream"
55 raw_length = response.headers.get("Content-Length")
56 content_length: int | None = None
57 if raw_length:
58 try:
59 content_length = int(raw_length)
60 except ValueError:
61 content_length = None
62
63 limited = response.read(max_bytes + 1)
64 except HttpDownloadError:
65 raise
66 except urllib.error.HTTPError as exc:
67 raise HttpDownloadError(f"下载失败,HTTP状态码: {exc.code}") from exc
68 except Exception as exc:
69 raise HttpDownloadError("下载文件失败") from exc
70
71 if len(limited) > max_bytes:
72 raise HttpDownloadError(f"文件大小超过限制({max_bytes // (1024 * 1024)}MB)")
73
74 return HttpDownloadResult(
75 data=limited,
76 content_type=content_type.split(";", 1)[0].strip() or "application/octet-stream",
77 content_length=content_length,
78 )
79
79 lines PYTHON