iflow2api 1.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iflow2api/__init__.py +3 -0
- iflow2api/__main__.py +6 -0
- iflow2api/admin/__init__.py +13 -0
- iflow2api/admin/auth.py +309 -0
- iflow2api/admin/routes.py +701 -0
- iflow2api/admin/static/css/style.css +644 -0
- iflow2api/admin/static/index.html +423 -0
- iflow2api/admin/static/js/app.js +703 -0
- iflow2api/admin/websocket.py +94 -0
- iflow2api/app.py +1675 -0
- iflow2api/autostart.py +242 -0
- iflow2api/config.py +177 -0
- iflow2api/crypto.py +381 -0
- iflow2api/gui.py +1058 -0
- iflow2api/i18n.py +120 -0
- iflow2api/instances.py +418 -0
- iflow2api/locales/en.json +114 -0
- iflow2api/locales/zh.json +114 -0
- iflow2api/main.py +6 -0
- iflow2api/oauth.py +209 -0
- iflow2api/oauth_login.py +154 -0
- iflow2api/proxy.py +642 -0
- iflow2api/ratelimit.py +291 -0
- iflow2api/server.py +152 -0
- iflow2api/settings.py +279 -0
- iflow2api/token_refresher.py +213 -0
- iflow2api/tray.py +210 -0
- iflow2api/updater.py +191 -0
- iflow2api/version.py +255 -0
- iflow2api/vision.py +459 -0
- iflow2api/web_server.py +340 -0
- iflow2api-1.4.7.dist-info/METADATA +495 -0
- iflow2api-1.4.7.dist-info/RECORD +36 -0
- iflow2api-1.4.7.dist-info/WHEEL +4 -0
- iflow2api-1.4.7.dist-info/entry_points.txt +3 -0
- iflow2api-1.4.7.dist-info/licenses/LICENSE +21 -0
iflow2api/__init__.py
ADDED
iflow2api/__main__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Web 管理界面模块"""
|
|
2
|
+
|
|
3
|
+
from .auth import AuthManager, create_access_token, verify_token
|
|
4
|
+
from .routes import admin_router
|
|
5
|
+
from .websocket import ConnectionManager
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AuthManager",
|
|
9
|
+
"create_access_token",
|
|
10
|
+
"verify_token",
|
|
11
|
+
"admin_router",
|
|
12
|
+
"ConnectionManager",
|
|
13
|
+
]
|
iflow2api/admin/auth.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Web 管理界面认证模块"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
# PBKDF2 哈希参数
|
|
16
|
+
_PBKDF2_ITERATIONS = 260000 # OWASP 2023 推荐值
|
|
17
|
+
_PBKDF2_HASH = "sha256"
|
|
18
|
+
_HASH_PREFIX = "pbkdf2:" # 用于区分新旧格式
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AdminUser(BaseModel):
|
|
22
|
+
"""管理员用户"""
|
|
23
|
+
username: str
|
|
24
|
+
password_hash: str
|
|
25
|
+
created_at: datetime
|
|
26
|
+
last_login: Optional[datetime] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TokenData(BaseModel):
|
|
30
|
+
"""Token 数据"""
|
|
31
|
+
username: str
|
|
32
|
+
exp: datetime
|
|
33
|
+
iat: datetime
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuthManager:
|
|
37
|
+
"""认证管理器"""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self._users: dict[str, AdminUser] = {}
|
|
41
|
+
self._active_tokens: dict[str, TokenData] = {}
|
|
42
|
+
self._config_path = Path.home() / ".iflow2api" / "admin_users.json"
|
|
43
|
+
# JWT secret 单独存储,与用户数据分离
|
|
44
|
+
self._jwt_secret_path = Path.home() / ".iflow2api" / ".jwt_secret"
|
|
45
|
+
self._jwt_secret = self._load_or_create_jwt_secret()
|
|
46
|
+
self._load_users()
|
|
47
|
+
|
|
48
|
+
def _load_or_create_jwt_secret(self) -> str:
|
|
49
|
+
"""加载或创建 JWT 签名密钥,存储在独立的权限严格文件中"""
|
|
50
|
+
if self._jwt_secret_path.exists():
|
|
51
|
+
try:
|
|
52
|
+
secret = self._jwt_secret_path.read_text(encoding="utf-8").strip()
|
|
53
|
+
if len(secret) >= 32:
|
|
54
|
+
return secret
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
# 生成新密钥
|
|
58
|
+
secret = secrets.token_hex(32)
|
|
59
|
+
self._jwt_secret_path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
self._jwt_secret_path.write_text(secret, encoding="utf-8")
|
|
61
|
+
try:
|
|
62
|
+
os.chmod(self._jwt_secret_path, 0o600)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
return secret
|
|
66
|
+
|
|
67
|
+
def _load_users(self) -> None:
|
|
68
|
+
"""加载用户数据"""
|
|
69
|
+
if self._config_path.exists():
|
|
70
|
+
try:
|
|
71
|
+
with open(self._config_path, "r", encoding="utf-8") as f:
|
|
72
|
+
data = json.load(f)
|
|
73
|
+
for username, user_data in data.get("users", {}).items():
|
|
74
|
+
self._users[username] = AdminUser(
|
|
75
|
+
username=username,
|
|
76
|
+
password_hash=user_data["password_hash"],
|
|
77
|
+
created_at=datetime.fromisoformat(user_data["created_at"]),
|
|
78
|
+
last_login=datetime.fromisoformat(user_data["last_login"])
|
|
79
|
+
if user_data.get("last_login") else None,
|
|
80
|
+
)
|
|
81
|
+
# 兼容旧版本:旧版 jwt_secret 存在 users 文件中,迁移到独立文件
|
|
82
|
+
if "jwt_secret" in data and not self._jwt_secret_path.exists():
|
|
83
|
+
secret = data["jwt_secret"]
|
|
84
|
+
self._jwt_secret_path.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
self._jwt_secret_path.write_text(secret, encoding="utf-8")
|
|
86
|
+
try:
|
|
87
|
+
os.chmod(self._jwt_secret_path, 0o600)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
self._jwt_secret = secret
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def _save_users(self) -> None:
|
|
95
|
+
"""保存用户数据(不包含 JWT secret,避免敏感信息共存)"""
|
|
96
|
+
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
data = {
|
|
98
|
+
"users": {
|
|
99
|
+
username: {
|
|
100
|
+
"password_hash": user.password_hash,
|
|
101
|
+
"created_at": user.created_at.isoformat(),
|
|
102
|
+
"last_login": user.last_login.isoformat() if user.last_login else None,
|
|
103
|
+
}
|
|
104
|
+
for username, user in self._users.items()
|
|
105
|
+
}
|
|
106
|
+
# jwt_secret 不再保存在此文件中
|
|
107
|
+
}
|
|
108
|
+
with open(self._config_path, "w", encoding="utf-8") as f:
|
|
109
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _hash_password(password: str) -> str:
|
|
113
|
+
"""哈希密码 - 使用 PBKDF2-HMAC-SHA256 加随机 salt(C-03 修复)
|
|
114
|
+
|
|
115
|
+
格式: pbkdf2:{salt_hex}:{hash_hex}
|
|
116
|
+
"""
|
|
117
|
+
salt = secrets.token_bytes(32)
|
|
118
|
+
dk = hashlib.pbkdf2_hmac(
|
|
119
|
+
_PBKDF2_HASH,
|
|
120
|
+
password.encode("utf-8"),
|
|
121
|
+
salt,
|
|
122
|
+
_PBKDF2_ITERATIONS,
|
|
123
|
+
)
|
|
124
|
+
return f"{_HASH_PREFIX}{salt.hex()}:{dk.hex()}"
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _verify_password(password: str, stored_hash: str) -> bool:
|
|
128
|
+
"""验证密码,兼容旧版 SHA-256 格式(无 salt)和新版 PBKDF2 格式
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
password: 明文密码
|
|
132
|
+
stored_hash: 存储的哈希值
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
密码是否匹配
|
|
136
|
+
"""
|
|
137
|
+
if stored_hash.startswith(_HASH_PREFIX):
|
|
138
|
+
# 新格式:pbkdf2:{salt_hex}:{hash_hex}
|
|
139
|
+
try:
|
|
140
|
+
_, salt_hex, hash_hex = stored_hash.split(":")
|
|
141
|
+
salt = bytes.fromhex(salt_hex)
|
|
142
|
+
expected = bytes.fromhex(hash_hex)
|
|
143
|
+
dk = hashlib.pbkdf2_hmac(
|
|
144
|
+
_PBKDF2_HASH,
|
|
145
|
+
password.encode("utf-8"),
|
|
146
|
+
salt,
|
|
147
|
+
_PBKDF2_ITERATIONS,
|
|
148
|
+
)
|
|
149
|
+
# 使用常数时间比较,防止时序攻击(C-04 修复)
|
|
150
|
+
return hmac.compare_digest(dk, expected)
|
|
151
|
+
except Exception:
|
|
152
|
+
return False
|
|
153
|
+
else:
|
|
154
|
+
# 旧格式:裸 SHA-256(向后兼容,登录成功后自动升级)
|
|
155
|
+
old_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
156
|
+
return hmac.compare_digest(stored_hash, old_hash)
|
|
157
|
+
|
|
158
|
+
def create_user(self, username: str, password: str) -> bool:
|
|
159
|
+
"""创建用户"""
|
|
160
|
+
if username in self._users:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
user = AdminUser(
|
|
164
|
+
username=username,
|
|
165
|
+
password_hash=self._hash_password(password),
|
|
166
|
+
created_at=datetime.now(),
|
|
167
|
+
)
|
|
168
|
+
self._users[username] = user
|
|
169
|
+
self._save_users()
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
def delete_user(self, username: str) -> bool:
|
|
173
|
+
"""删除用户"""
|
|
174
|
+
if username not in self._users:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
del self._users[username]
|
|
178
|
+
# 清除该用户的所有 token
|
|
179
|
+
tokens_to_remove = [
|
|
180
|
+
token for token, data in self._active_tokens.items()
|
|
181
|
+
if data.username == username
|
|
182
|
+
]
|
|
183
|
+
for token in tokens_to_remove:
|
|
184
|
+
del self._active_tokens[token]
|
|
185
|
+
|
|
186
|
+
self._save_users()
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
def change_password(self, username: str, old_password: str, new_password: str) -> bool:
|
|
190
|
+
"""修改密码"""
|
|
191
|
+
if username not in self._users:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
user = self._users[username]
|
|
195
|
+
if not self._verify_password(old_password, user.password_hash):
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
user.password_hash = self._hash_password(new_password)
|
|
199
|
+
# 清除该用户的所有 token,强制重新登录
|
|
200
|
+
tokens_to_remove = [
|
|
201
|
+
token for token, data in self._active_tokens.items()
|
|
202
|
+
if data.username == username
|
|
203
|
+
]
|
|
204
|
+
for token in tokens_to_remove:
|
|
205
|
+
del self._active_tokens[token]
|
|
206
|
+
|
|
207
|
+
self._save_users()
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
def authenticate(self, username: str, password: str) -> Optional[str]:
|
|
211
|
+
"""验证用户并返回 token,登录成功后自动升级旧密码哈希格式"""
|
|
212
|
+
if username not in self._users:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
user = self._users[username]
|
|
216
|
+
if not self._verify_password(password, user.password_hash):
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
# 旧格式哈希自动升级为 PBKDF2(C-03 修复)
|
|
220
|
+
if not user.password_hash.startswith(_HASH_PREFIX):
|
|
221
|
+
user.password_hash = self._hash_password(password)
|
|
222
|
+
|
|
223
|
+
# 更新最后登录时间
|
|
224
|
+
user.last_login = datetime.now()
|
|
225
|
+
self._save_users()
|
|
226
|
+
|
|
227
|
+
# 创建 token
|
|
228
|
+
token = create_access_token(username, self._jwt_secret)
|
|
229
|
+
self._active_tokens[token] = TokenData(
|
|
230
|
+
username=username,
|
|
231
|
+
exp=datetime.now() + timedelta(hours=24),
|
|
232
|
+
iat=datetime.now(),
|
|
233
|
+
)
|
|
234
|
+
return token
|
|
235
|
+
|
|
236
|
+
def verify_token(self, token: str) -> Optional[str]:
|
|
237
|
+
"""验证 token 并返回用户名"""
|
|
238
|
+
if token not in self._active_tokens:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
token_data = self._active_tokens[token]
|
|
242
|
+
if datetime.now() > token_data.exp:
|
|
243
|
+
del self._active_tokens[token]
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
return token_data.username
|
|
247
|
+
|
|
248
|
+
def logout(self, token: str) -> bool:
|
|
249
|
+
"""登出"""
|
|
250
|
+
if token in self._active_tokens:
|
|
251
|
+
del self._active_tokens[token]
|
|
252
|
+
return True
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
def get_users(self) -> list[dict]:
|
|
256
|
+
"""获取所有用户列表"""
|
|
257
|
+
return [
|
|
258
|
+
{
|
|
259
|
+
"username": user.username,
|
|
260
|
+
"created_at": user.created_at.isoformat(),
|
|
261
|
+
"last_login": user.last_login.isoformat() if user.last_login else None,
|
|
262
|
+
}
|
|
263
|
+
for user in self._users.values()
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
def has_users(self) -> bool:
|
|
267
|
+
"""检查是否有用户"""
|
|
268
|
+
return len(self._users) > 0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def create_access_token(username: str, secret: str) -> str:
|
|
272
|
+
"""创建访问令牌"""
|
|
273
|
+
timestamp = str(int(time.time() * 1000))
|
|
274
|
+
random_part = secrets.token_hex(16)
|
|
275
|
+
data = f"{username}:{timestamp}:{random_part}"
|
|
276
|
+
signature = hashlib.sha256(f"{data}:{secret}".encode()).hexdigest()[:32]
|
|
277
|
+
return f"{data}:{signature}"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def verify_token(token: str, secret: str) -> Optional[str]:
|
|
281
|
+
"""验证令牌并返回用户名"""
|
|
282
|
+
try:
|
|
283
|
+
parts = token.split(":")
|
|
284
|
+
if len(parts) != 4:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
username, timestamp, random_part, signature = parts
|
|
288
|
+
data = f"{username}:{timestamp}:{random_part}"
|
|
289
|
+
expected_signature = hashlib.sha256(f"{data}:{secret}".encode()).hexdigest()[:32]
|
|
290
|
+
|
|
291
|
+
# 使用常数时间比较,防止时序攻击(C-04 修复)
|
|
292
|
+
if not hmac.compare_digest(signature, expected_signature):
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
return username
|
|
296
|
+
except Exception:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# 全局认证管理器实例
|
|
301
|
+
_auth_manager: Optional[AuthManager] = None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def get_auth_manager() -> AuthManager:
|
|
305
|
+
"""获取全局认证管理器实例"""
|
|
306
|
+
global _auth_manager
|
|
307
|
+
if _auth_manager is None:
|
|
308
|
+
_auth_manager = AuthManager()
|
|
309
|
+
return _auth_manager
|