Breaking changes: - Same seeds now yield different prompts than 5.x (reproducibility fixed) - Civitai Trending: removed broken 'model' filter widget - Shutdown node is a no-op unless ISULION_ENABLE_SHUTDOWN=1 Highlights: - Seeding: all 109 theme handlers share the seeded ConfigManager PRNG (284 global random.* call sites migrated); same seed => same prompt - Theme registry rewritten with @register_theme decorators; removed HANDLER_CLASS_NAMES_MAP, star-imports and globals() lookups - Shared ThemeRegistry singleton: INPUT_TYPES no longer re-reads ~100 JSON files per call (2.6ms -> 0.003ms cached) - load_images_node: paths sandboxed to ComfyUI input dir, deterministic sha1 IS_CHANGED, PIL context managers, no bare excepts - Overlay node: full batch support, base channel count preserved (RGB in -> RGB out), pass-through on internal errors - QR node: vectorized module drawing (np.kron), logging instead of prints - New net_utils.safe_download: http(s)-only scheme + 20MB cap for URL fetching nodes (SSRF/decompression-bomb hardening) - Removed unused soft_empty_cache() calls, dead code (culinary_handler, IsulionCategorySelector, orphan Core_Nodes/config dir, dead js ext), invalid WEB_DIRECTORY; fixed display-name mappings - s300 handler: fixed call to non-existent config method (latent crash) - Added MIT LICENSE and requirements.txt (requests)
38 lines
1.4 KiB
Python
38 lines
1.4 KiB
Python
"""Shared, hardened download helper for URL-fetching nodes."""
|
|
|
|
import logging
|
|
from urllib.parse import urlparse
|
|
|
|
import requests
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MAX_IMAGE_BYTES = 20 * 1024 * 1024 # 20 MB safety cap (decompression-bomb guard)
|
|
|
|
|
|
class DownloadError(Exception):
|
|
"""Raised when a URL fails scheme validation or exceeds size limits."""
|
|
|
|
|
|
def validate_url(url: str) -> None:
|
|
"""Reject non-http(s) or hostless URLs before any network call."""
|
|
parsed = urlparse(str(url))
|
|
if parsed.scheme not in ("http", "https"):
|
|
raise DownloadError(f"URL scheme not allowed: {parsed.scheme!r}")
|
|
if not parsed.netloc:
|
|
raise DownloadError("URL has no host")
|
|
|
|
|
|
def safe_download(url: str, timeout: int = 15, max_bytes: int = MAX_IMAGE_BYTES) -> bytes:
|
|
"""Download `url` enforcing http(s)-only schemes and a hard size cap."""
|
|
validate_url(url)
|
|
with requests.get(url, timeout=timeout, stream=True) as response:
|
|
response.raise_for_status()
|
|
content_length = response.headers.get("Content-Length")
|
|
if content_length is not None and int(content_length) > max_bytes:
|
|
raise DownloadError(f"Content too large ({content_length} bytes > {max_bytes})")
|
|
data = response.raw.read(max_bytes + 1, decode_content=True)
|
|
if len(data) > max_bytes:
|
|
raise DownloadError(f"Content exceeded {max_bytes} bytes while streaming")
|
|
return data
|