Skip to content

Commit a752345

Browse files
committed
添加对 imgproxy 的支持,新增 URL 加密功能以增强图像处理模块的安全性,同时更新工作流配置以启用图像预览功能。
1 parent 13aed0d commit a752345

3 files changed

Lines changed: 183 additions & 3 deletions

File tree

handlers/commands/image.py

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import asyncio
2+
import base64
3+
import hmac
4+
import hashlib
5+
import os
26
from configparser import ConfigParser
37
from dataclasses import dataclass
48
from datetime import datetime, timedelta
59
from typing import Optional, List, Dict, Any, Tuple
10+
from urllib.parse import urlparse
611
import uuid
712

13+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
14+
from cryptography.hazmat.backends import default_backend
15+
816
from aiogram import types
917
from aiogram.exceptions import TelegramBadRequest
1018
from aiogram.filters import Command
@@ -18,6 +26,152 @@
1826

1927
logger = manager.logger
2028

29+
30+
def encrypt_url_for_imgproxy(url: str, encryption_key: str) -> str:
31+
"""
32+
使用 AES-256-CBC 加密 URL 用于 imgproxy
33+
34+
Args:
35+
url: 要加密的原始 URL
36+
encryption_key: imgproxy 源 URL 加密密钥(base64 编码或原始字符串)
37+
38+
Returns:
39+
base64url 编码的加密 URL(包含 IV + 密文)
40+
"""
41+
try:
42+
# 尝试将密钥作为 base64 解码(imgproxy 标准格式)
43+
try:
44+
key_bytes = base64.b64decode(encryption_key)
45+
except Exception:
46+
# 如果解码失败,作为普通字符串使用,并确保是32字节
47+
key_bytes = encryption_key.encode('utf-8')
48+
if len(key_bytes) != 32:
49+
# 如果长度不对,使用 SHA256 哈希
50+
key_bytes = hashlib.sha256(key_bytes).digest()
51+
52+
# 确保密钥是32字节(AES-256需要32字节密钥)
53+
if len(key_bytes) != 32:
54+
raise ValueError(f"加密密钥必须是32字节,当前为{len(key_bytes)}字节")
55+
56+
# 生成16字节的随机IV(AES-CBC需要16字节IV)
57+
iv = os.urandom(16)
58+
59+
# 创建加密器
60+
cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend())
61+
encryptor = cipher.encryptor()
62+
63+
# 对URL进行PKCS7填充
64+
url_bytes = url.encode('utf-8')
65+
pad_length = 16 - (len(url_bytes) % 16)
66+
padded_url = url_bytes + bytes([pad_length] * pad_length)
67+
68+
# 加密
69+
ciphertext = encryptor.update(padded_url) + encryptor.finalize()
70+
71+
# 组合 IV + 密文,然后使用 base64url 编码
72+
encrypted_data = iv + ciphertext
73+
encrypted_url = base64.urlsafe_b64encode(encrypted_data).decode('utf-8').rstrip('=')
74+
75+
return encrypted_url
76+
except Exception as e:
77+
logger.error(f"URL加密失败: {e}")
78+
raise
79+
80+
81+
def generate_imgproxy_url(original_url: str, domain: str, key: str, salt: str = "", encryption_key: Optional[str] = None) -> str:
82+
"""
83+
使用 imgproxy 生成签名 URL 或加密 URL
84+
85+
Args:
86+
original_url: 原始图片 URL (如 local://comfy/subfolder/filename)
87+
domain: imgproxy 域名(如 https://img.iscys.com)
88+
key: imgproxy 签名密钥(加密模式下不使用)
89+
salt: imgproxy 签名盐值(可选,加密模式下不使用)
90+
encryption_key: imgproxy 源 URL 加密密钥(如果提供,将使用加密模式)
91+
92+
Returns:
93+
签名或加密后的 imgproxy URL
94+
"""
95+
if not domain:
96+
# 如果配置缺失,返回原始 URL
97+
return original_url
98+
99+
# 如果提供了加密密钥,使用加密模式
100+
if encryption_key:
101+
try:
102+
encrypted_url = encrypt_url_for_imgproxy(original_url, encryption_key)
103+
domain = domain.rstrip('/')
104+
# 加密URL格式: {domain}/{encrypted_url}
105+
imgproxy_url = f"{domain}/{encrypted_url}"
106+
return imgproxy_url
107+
except Exception as e:
108+
logger.warning(f"URL加密失败,回退到签名模式: {e}")
109+
# 如果加密失败,回退到签名模式
110+
111+
# 使用签名模式(原有逻辑)
112+
if not key:
113+
# 如果配置缺失,返回原始 URL
114+
return original_url
115+
116+
# 处理 local:// 协议的 URL
117+
# local://comfy/subfolder/filename -> /local://comfy/subfolder/filename
118+
# imgproxy 需要将完整 URL 作为路径的一部分
119+
if original_url.startswith("local://"):
120+
# 对于 local:// 协议,将整个 URL 作为路径
121+
path = f"/{original_url}"
122+
else:
123+
# 对于其他协议,提取路径部分
124+
try:
125+
parsed = urlparse(original_url)
126+
path = parsed.path
127+
if not path.startswith("/"):
128+
path = f"/{path}"
129+
except Exception:
130+
# 如果解析失败,尝试简单提取
131+
if "://" in original_url:
132+
parts = original_url.split("://", 1)[1].split("/", 1)
133+
path = f"/{parts[1]}" if len(parts) > 1 else "/"
134+
else:
135+
path = original_url if original_url.startswith("/") else f"/{original_url}"
136+
137+
# 使用 HMAC-SHA256 生成签名
138+
# imgproxy 标准:key 和 salt 通常是 base64 编码的二进制数据
139+
try:
140+
# 尝试将 key 作为 base64 解码(imgproxy 标准格式)
141+
key_bytes = base64.b64decode(key)
142+
except Exception:
143+
# 如果解码失败,作为普通字符串使用
144+
key_bytes = key.encode('utf-8')
145+
146+
salt_bytes = b''
147+
if salt:
148+
try:
149+
# 尝试将 salt 作为 base64 解码(imgproxy 标准格式)
150+
salt_bytes = base64.b64decode(salt)
151+
except Exception:
152+
# 如果解码失败,作为普通字符串使用
153+
salt_bytes = salt.encode('utf-8')
154+
155+
# imgproxy 标准签名算法
156+
if salt_bytes:
157+
# 如果提供了 salt:HMAC-SHA256(key, HMAC-SHA256(salt, path))
158+
inner_hash = hmac.new(salt_bytes, path.encode('utf-8'), hashlib.sha256).digest()
159+
signature = hmac.new(key_bytes, inner_hash, hashlib.sha256).digest()
160+
else:
161+
# 如果没有 salt,直接使用 key 签名
162+
signature = hmac.new(key_bytes, path.encode('utf-8'), hashlib.sha256).digest()
163+
164+
# 使用 base64url 编码(URL-safe base64,去掉填充的 =)
165+
signature_b64 = base64.urlsafe_b64encode(signature).decode('utf-8').rstrip('=')
166+
167+
# 构建 imgproxy URL: {domain}/{signature}{path}
168+
# 确保 domain 不以 / 结尾,path 以 / 开头
169+
domain = domain.rstrip('/')
170+
imgproxy_url = f"{domain}/{signature_b64}{path}"
171+
172+
return imgproxy_url
173+
174+
21175
# 常量定义
22176
GLOBAL_TASK_LIMIT = 3
23177
QUEUE_NAME = "txt2img"
@@ -600,7 +754,32 @@ async def handle_completed_task(task: Task, endpoint: str, prefix: str, rdb):
600754
size = task.options.get("size", DEFAULT_SIZE)
601755
step = task.options.get("step", DEFAULT_STEP)
602756

603-
image_url = f"https://one.iscys.com/Comfy/{subfolder}/{filename}"
757+
reply_markup = None
758+
759+
# 使用 imgproxy 处理 URL
760+
try:
761+
# 生成原始图片 URL (格式: local://comfy/subfolder/filename)
762+
original_url = f"local://comfy/{subfolder}/{filename}"
763+
764+
# 从配置读取 imgproxy 参数
765+
imgproxy_domain = manager.config["imgproxy"]["domain"]
766+
imgproxy_key = manager.config["imgproxy"].get("imgproxy_key", "")
767+
imgproxy_salt = manager.config["imgproxy"].get("imgproxy_salt", "")
768+
imgproxy_encryption_key = manager.config["imgproxy"].get("imgproxy_source_url_encryption_key", "")
769+
770+
# 生成 imgproxy URL(如果提供了加密密钥,将使用加密模式)
771+
image_url = generate_imgproxy_url(
772+
original_url,
773+
imgproxy_domain,
774+
imgproxy_key,
775+
imgproxy_salt,
776+
imgproxy_encryption_key if imgproxy_encryption_key else None
777+
)
778+
reply_markup = types.InlineKeyboardMarkup(inline_keyboard=[[types.InlineKeyboardButton(text="Original|原始图片", url=image_url)]])
779+
except (KeyError, ValueError) as e:
780+
# 如果配置不存在,不使用按钮
781+
logger.warning(f"{prefix} imgproxy config error: {e}, skipping imgproxy URL")
782+
reply_markup = None
604783

605784
if task.msg.reply_message_id != -1:
606785
await manager.bot.edit_message_text(
@@ -618,7 +797,7 @@ async def handle_completed_task(task: Task, endpoint: str, prefix: str, rdb):
618797
photo=input_file,
619798
reply_to_message_id=task.msg.message_id,
620799
caption=caption,
621-
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[[types.InlineKeyboardButton(text="Original|原始图片", url=image_url)]]),
800+
reply_markup=reply_markup,
622801
)
623802
else:
624803
await manager.bot.send_photo(

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ edge-tts==6.1.8
1212
aiosqlite==0.20.0
1313

1414
beautifulsoup4==4.12.3
15+
cryptography
1516

1617
# testing
1718
pytest

utils/comfy_workflow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@
216216
"save_workflow_as_json": False,
217217
"counter": 0,
218218
"time_format": "%Y-%m-%d-%H%M%S",
219-
"show_preview": False,
219+
"show_preview": True,
220220
"images": ["46", 0],
221221
},
222222
"class_type": "Image Saver Simple",

0 commit comments

Comments
 (0)