11import asyncio
2+ import base64
3+ import hmac
4+ import hashlib
5+ import os
26from configparser import ConfigParser
37from dataclasses import dataclass
48from datetime import datetime , timedelta
59from typing import Optional , List , Dict , Any , Tuple
10+ from urllib .parse import urlparse
611import uuid
712
13+ from cryptography .hazmat .primitives .ciphers import Cipher , algorithms , modes
14+ from cryptography .hazmat .backends import default_backend
15+
816from aiogram import types
917from aiogram .exceptions import TelegramBadRequest
1018from aiogram .filters import Command
1826
1927logger = 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# 常量定义
22176GLOBAL_TASK_LIMIT = 3
23177QUEUE_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 (
0 commit comments