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 ADDED
@@ -0,0 +1,3 @@
1
+ """iflow2api - 将 iFlow CLI 的 AI 服务暴露为 OpenAI 兼容 API"""
2
+
3
+ __version__ = "1.4.7"
iflow2api/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """iflow2api 命令行入口"""
2
+
3
+ from .app import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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
+ ]
@@ -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