Update api.txt
Browse files
api.txt
CHANGED
|
@@ -13,9 +13,65 @@ from typing import Dict
|
|
| 13 |
from loguru import logger
|
| 14 |
from starlette.responses import StreamingResponse
|
| 15 |
import socket
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
app = FastAPI()
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# 浏览器实例管理
|
| 20 |
BROWSERS: Dict[str, dict] = {} # {browser_id: {"process": Popen, "ws": str, "port": int, "status": str, "info": dict}}
|
| 21 |
# 浏览器实例状态: open, closed
|
|
@@ -198,10 +254,12 @@ async def system_usage():
|
|
| 198 |
"""
|
| 199 |
try:
|
| 200 |
usage_info = get_system_usage()
|
| 201 |
-
|
|
|
|
| 202 |
except Exception as e:
|
| 203 |
logger.error(f"获取系统资源使用情况失败: {e}")
|
| 204 |
-
|
|
|
|
| 205 |
|
| 206 |
|
| 207 |
# BitBrowser风格API
|
|
@@ -214,12 +272,13 @@ async def open_browser(request: Request):
|
|
| 214 |
if browser_id in BROWSERS:
|
| 215 |
logger.info(f"Browser ID {browser_id}: {BROWSERS[browser_id]}")
|
| 216 |
if BROWSERS[browser_id]["status"] == "open":
|
| 217 |
-
|
| 218 |
"success": True,
|
| 219 |
"msg": "already opened",
|
| 220 |
"data": {"id": browser_id, "ws": BROWSERS[browser_id]["ws"]},
|
| 221 |
"info": BROWSERS[browser_id]["info"]
|
| 222 |
}
|
|
|
|
| 223 |
else:
|
| 224 |
port = get_free_port()
|
| 225 |
BROWSERS[browser_id]["port"] = port
|
|
@@ -253,7 +312,8 @@ async def open_browser(request: Request):
|
|
| 253 |
proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 254 |
if not wait_port("127.0.0.1", port, timeout=60):
|
| 255 |
proc.terminate()
|
| 256 |
-
|
|
|
|
| 257 |
async with httpx.AsyncClient() as client:
|
| 258 |
resp = await client.get(f"http://127.0.0.1:{port}/json/version")
|
| 259 |
if resp.status_code == 200:
|
|
@@ -263,7 +323,8 @@ async def open_browser(request: Request):
|
|
| 263 |
ws_url = re.sub(r":\d+", "", ws_url)
|
| 264 |
logger.info(f"Browser opened with ID: {browser_id}, Info: {browser_info}")
|
| 265 |
BROWSERS[browser_id] = {"process": proc, "ws": ws_url, "port": port, "status": "open", "info": browser_info}
|
| 266 |
-
|
|
|
|
| 267 |
|
| 268 |
|
| 269 |
@app.post("/browser/close")
|
|
@@ -272,19 +333,23 @@ async def close_browser(request: Request):
|
|
| 272 |
browser_id = data.get("id")
|
| 273 |
b = BROWSERS.get(browser_id)
|
| 274 |
if not b:
|
| 275 |
-
|
|
|
|
| 276 |
b["process"].terminate()
|
| 277 |
b["status"] = "closed"
|
| 278 |
-
|
|
|
|
| 279 |
|
| 280 |
|
|
|
|
| 281 |
@app.post("/browser/delete")
|
| 282 |
async def delete_browser(request: Request):
|
| 283 |
data = await request.json()
|
| 284 |
browser_id = data.get("id")
|
| 285 |
b = BROWSERS.pop(browser_id, None)
|
| 286 |
if not b:
|
| 287 |
-
|
|
|
|
| 288 |
try:
|
| 289 |
b["process"].terminate()
|
| 290 |
except Exception:
|
|
@@ -294,7 +359,8 @@ async def delete_browser(request: Request):
|
|
| 294 |
if os.path.exists(profile_dir):
|
| 295 |
import shutil
|
| 296 |
shutil.rmtree(profile_dir, ignore_errors=True)
|
| 297 |
-
|
|
|
|
| 298 |
|
| 299 |
|
| 300 |
# @app.post("/browser/update")
|
|
@@ -316,12 +382,48 @@ async def delete_browser(request: Request):
|
|
| 316 |
async def browser_ports():
|
| 317 |
# 返回 {id: port} 格式
|
| 318 |
data = {k: str(v["ws"]) for k, v in BROWSERS.items() if v["status"] == "open"}
|
| 319 |
-
|
|
|
|
| 320 |
|
| 321 |
|
| 322 |
@app.post("/health")
|
| 323 |
async def health():
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
|
| 327 |
CDP_PATHS = [
|
|
|
|
| 13 |
from loguru import logger
|
| 14 |
from starlette.responses import StreamingResponse
|
| 15 |
import socket
|
| 16 |
+
import base64
|
| 17 |
+
from cryptography.fernet import Fernet
|
| 18 |
+
from cryptography.hazmat.primitives import hashes
|
| 19 |
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
| 20 |
|
| 21 |
app = FastAPI()
|
| 22 |
|
| 23 |
+
# 加密配置
|
| 24 |
+
ENCRYPTION_KEY = os.getenv('ENCRYPTION_KEY', 'default-encryption-key-change-in-production') # 生产环境中应该使用环境变量
|
| 25 |
+
SALT = b'stable_salt_123456' # 固定salt,生产环境中应该更安全
|
| 26 |
+
|
| 27 |
+
def get_encryption_key():
|
| 28 |
+
"""生成加密密钥"""
|
| 29 |
+
kdf = PBKDF2HMAC(
|
| 30 |
+
algorithm=hashes.SHA256(),
|
| 31 |
+
length=32,
|
| 32 |
+
salt=SALT,
|
| 33 |
+
iterations=100000,
|
| 34 |
+
)
|
| 35 |
+
key = base64.urlsafe_b64encode(kdf.derive(ENCRYPTION_KEY.encode()))
|
| 36 |
+
return key
|
| 37 |
+
|
| 38 |
+
def encrypt_data(data: str) -> str:
|
| 39 |
+
"""加密数据"""
|
| 40 |
+
try:
|
| 41 |
+
key = get_encryption_key()
|
| 42 |
+
f = Fernet(key)
|
| 43 |
+
encrypted_data = f.encrypt(data.encode())
|
| 44 |
+
return base64.urlsafe_b64encode(encrypted_data).decode()
|
| 45 |
+
except Exception as e:
|
| 46 |
+
logger.error(f"加密失败: {e}")
|
| 47 |
+
return data
|
| 48 |
+
|
| 49 |
+
def decrypt_data(encrypted_data: str) -> str:
|
| 50 |
+
"""解密数据"""
|
| 51 |
+
try:
|
| 52 |
+
key = get_encryption_key()
|
| 53 |
+
f = Fernet(key)
|
| 54 |
+
encrypted_bytes = base64.urlsafe_b64decode(encrypted_data.encode())
|
| 55 |
+
decrypted_data = f.decrypt(encrypted_bytes)
|
| 56 |
+
return decrypted_data.decode()
|
| 57 |
+
except Exception as e:
|
| 58 |
+
logger.error(f"解密失败: {e}")
|
| 59 |
+
return encrypted_data
|
| 60 |
+
|
| 61 |
+
def encrypt_json_response(data: dict) -> dict:
|
| 62 |
+
"""加密JSON响应数据"""
|
| 63 |
+
try:
|
| 64 |
+
json_str = json.dumps(data, ensure_ascii=False)
|
| 65 |
+
encrypted_str = encrypt_data(json_str)
|
| 66 |
+
return {
|
| 67 |
+
"encrypted": True,
|
| 68 |
+
"data": encrypted_str,
|
| 69 |
+
"timestamp": int(time.time())
|
| 70 |
+
}
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"加密JSON响应失败: {e}")
|
| 73 |
+
return data
|
| 74 |
+
|
| 75 |
# 浏览器实例管理
|
| 76 |
BROWSERS: Dict[str, dict] = {} # {browser_id: {"process": Popen, "ws": str, "port": int, "status": str, "info": dict}}
|
| 77 |
# 浏览器实例状态: open, closed
|
|
|
|
| 254 |
"""
|
| 255 |
try:
|
| 256 |
usage_info = get_system_usage()
|
| 257 |
+
response_data = {"success": True, "msg": "success", "data": usage_info}
|
| 258 |
+
return encrypt_json_response(response_data)
|
| 259 |
except Exception as e:
|
| 260 |
logger.error(f"获取系统资源使用情况失败: {e}")
|
| 261 |
+
error_data = {"success": False, "msg": "failed", "error": str(e)}
|
| 262 |
+
return encrypt_json_response(error_data)
|
| 263 |
|
| 264 |
|
| 265 |
# BitBrowser风格API
|
|
|
|
| 272 |
if browser_id in BROWSERS:
|
| 273 |
logger.info(f"Browser ID {browser_id}: {BROWSERS[browser_id]}")
|
| 274 |
if BROWSERS[browser_id]["status"] == "open":
|
| 275 |
+
response_data = {
|
| 276 |
"success": True,
|
| 277 |
"msg": "already opened",
|
| 278 |
"data": {"id": browser_id, "ws": BROWSERS[browser_id]["ws"]},
|
| 279 |
"info": BROWSERS[browser_id]["info"]
|
| 280 |
}
|
| 281 |
+
return encrypt_json_response(response_data)
|
| 282 |
else:
|
| 283 |
port = get_free_port()
|
| 284 |
BROWSERS[browser_id]["port"] = port
|
|
|
|
| 312 |
proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 313 |
if not wait_port("127.0.0.1", port, timeout=60):
|
| 314 |
proc.terminate()
|
| 315 |
+
error_data = {"success": False, "msg": f"chrome端口{port}未就绪", "data": {}}
|
| 316 |
+
return encrypt_json_response(error_data)
|
| 317 |
async with httpx.AsyncClient() as client:
|
| 318 |
resp = await client.get(f"http://127.0.0.1:{port}/json/version")
|
| 319 |
if resp.status_code == 200:
|
|
|
|
| 323 |
ws_url = re.sub(r":\d+", "", ws_url)
|
| 324 |
logger.info(f"Browser opened with ID: {browser_id}, Info: {browser_info}")
|
| 325 |
BROWSERS[browser_id] = {"process": proc, "ws": ws_url, "port": port, "status": "open", "info": browser_info}
|
| 326 |
+
response_data = {"success": True, "msg": "success", "data": {"id": browser_id, "ws": ws_url}, "info": browser_info}
|
| 327 |
+
return encrypt_json_response(response_data)
|
| 328 |
|
| 329 |
|
| 330 |
@app.post("/browser/close")
|
|
|
|
| 333 |
browser_id = data.get("id")
|
| 334 |
b = BROWSERS.get(browser_id)
|
| 335 |
if not b:
|
| 336 |
+
error_data = {"success": False, "msg": "not found"}
|
| 337 |
+
return encrypt_json_response(error_data)
|
| 338 |
b["process"].terminate()
|
| 339 |
b["status"] = "closed"
|
| 340 |
+
response_data = {"success": True, "msg": "closed", "data": {"id": browser_id}}
|
| 341 |
+
return encrypt_json_response(response_data)
|
| 342 |
|
| 343 |
|
| 344 |
+
@app.post("/browser/delete")
|
| 345 |
@app.post("/browser/delete")
|
| 346 |
async def delete_browser(request: Request):
|
| 347 |
data = await request.json()
|
| 348 |
browser_id = data.get("id")
|
| 349 |
b = BROWSERS.pop(browser_id, None)
|
| 350 |
if not b:
|
| 351 |
+
error_data = {"success": False, "msg": "not found"}
|
| 352 |
+
return encrypt_json_response(error_data)
|
| 353 |
try:
|
| 354 |
b["process"].terminate()
|
| 355 |
except Exception:
|
|
|
|
| 359 |
if os.path.exists(profile_dir):
|
| 360 |
import shutil
|
| 361 |
shutil.rmtree(profile_dir, ignore_errors=True)
|
| 362 |
+
response_data = {"success": True, "msg": "deleted", "data": {"id": browser_id}}
|
| 363 |
+
return encrypt_json_response(response_data)
|
| 364 |
|
| 365 |
|
| 366 |
# @app.post("/browser/update")
|
|
|
|
| 382 |
async def browser_ports():
|
| 383 |
# 返回 {id: port} 格式
|
| 384 |
data = {k: str(v["ws"]) for k, v in BROWSERS.items() if v["status"] == "open"}
|
| 385 |
+
response_data = {"success": True, "data": data}
|
| 386 |
+
return encrypt_json_response(response_data)
|
| 387 |
|
| 388 |
|
| 389 |
@app.post("/health")
|
| 390 |
async def health():
|
| 391 |
+
response_data = {"success": True, "msg": "ok"}
|
| 392 |
+
return encrypt_json_response(response_data)
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
@app.post("/decrypt")
|
| 396 |
+
async def decrypt_response(request: Request):
|
| 397 |
+
"""
|
| 398 |
+
解密接口,用于客户端解密响应数据
|
| 399 |
+
"""
|
| 400 |
+
try:
|
| 401 |
+
data = await request.json()
|
| 402 |
+
encrypted_data = data.get("data")
|
| 403 |
+
if not encrypted_data:
|
| 404 |
+
return {"success": False, "msg": "missing encrypted data"}
|
| 405 |
+
|
| 406 |
+
decrypted_str = decrypt_data(encrypted_data)
|
| 407 |
+
decrypted_json = json.loads(decrypted_str)
|
| 408 |
+
return {"success": True, "msg": "decrypted", "data": decrypted_json}
|
| 409 |
+
except Exception as e:
|
| 410 |
+
logger.error(f"解密失败: {e}")
|
| 411 |
+
return {"success": False, "msg": "decrypt failed", "error": str(e)}
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
# 添加获取加密配置的接口(仅用于开发调试)
|
| 415 |
+
@app.get("/encryption/info")
|
| 416 |
+
async def encryption_info():
|
| 417 |
+
"""
|
| 418 |
+
返回加密相关信息(用于开发调试)
|
| 419 |
+
"""
|
| 420 |
+
return {
|
| 421 |
+
"encrypted": True,
|
| 422 |
+
"algorithm": "Fernet (AES 128)",
|
| 423 |
+
"key_derivation": "PBKDF2HMAC with SHA256",
|
| 424 |
+
"iterations": 100000,
|
| 425 |
+
"info": "All JSON responses are encrypted. Use /decrypt endpoint to decrypt."
|
| 426 |
+
}
|
| 427 |
|
| 428 |
|
| 429 |
CDP_PATHS = [
|