规范/对接/Hook/OpenAPI/交互图分目录;Hub 与机擎 Skill 路径同步;Obsidian 子目录配色。 Co-authored-by: Cursor <cursoragent@cursor.com>
254 lines
8.7 KiB
Python
254 lines
8.7 KiB
Python
"""
|
||
Hook 模块管理路由
|
||
对标文档:开发文档/5、接口/03-Hook与微信/Hook模块管理接口.md
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from fastapi import APIRouter, File, HTTPException, UploadFile, WebSocket, WebSocketDisconnect
|
||
from fastapi.responses import FileResponse
|
||
from pydantic import BaseModel, Field
|
||
from typing import List, Optional
|
||
|
||
from services.adb_device import adb_manager
|
||
from services.hook_module_service import hook_module_service
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
class ModuleUpsertRequest(BaseModel):
|
||
module_id: str
|
||
name: str
|
||
version: str = "1.0.0"
|
||
description: str = ""
|
||
scopes: List[str] = Field(default_factory=lambda: ["com.tencent.mm"])
|
||
capabilities: List[str] = Field(default_factory=list)
|
||
min_frida_version: str = "16.0.0"
|
||
script_content: Optional[str] = None
|
||
enabled: bool = True
|
||
|
||
|
||
class ScopeUpdateRequest(BaseModel):
|
||
scopes: List[str]
|
||
|
||
|
||
class DeployScriptRequest(BaseModel):
|
||
device_ids: List[str]
|
||
auto_reload: bool = True
|
||
|
||
|
||
class ReloadDeviceModulesRequest(BaseModel):
|
||
module_ids: List[str]
|
||
force: bool = False
|
||
|
||
|
||
def _probe_hook_ready(device_id: str) -> dict:
|
||
dev = adb_manager.get_device(device_id)
|
||
if not dev or not dev.is_online():
|
||
return {
|
||
"supports_hook": False,
|
||
"root_status": False,
|
||
"frida_version": "",
|
||
"detail": "device offline",
|
||
}
|
||
root_status = False
|
||
frida_version = ""
|
||
detail = "ok"
|
||
try:
|
||
uid = dev._shell("id -u", timeout=4)
|
||
su_path = dev._shell("which su", timeout=4)
|
||
root_status = (uid.strip() == "0") or bool(su_path.strip())
|
||
except Exception:
|
||
pass
|
||
try:
|
||
frida_version = dev._shell("frida-server --version", timeout=4).strip()
|
||
except Exception:
|
||
frida_version = ""
|
||
# 常见部署把 frida-server 放在 /data/local/tmp,不在默认 PATH
|
||
if not frida_version:
|
||
try:
|
||
frida_version = dev._shell("/data/local/tmp/frida-server --version", timeout=4).strip()
|
||
except Exception:
|
||
frida_version = ""
|
||
supports_hook = bool(root_status and frida_version)
|
||
if not root_status:
|
||
detail = "root unavailable"
|
||
elif not frida_version:
|
||
detail = "frida-server unavailable"
|
||
return {
|
||
"supports_hook": supports_hook,
|
||
"root_status": root_status,
|
||
"frida_version": frida_version,
|
||
"detail": detail,
|
||
}
|
||
|
||
|
||
@router.get("/modules")
|
||
async def list_modules(enabled: Optional[bool] = None, scope: Optional[str] = None):
|
||
modules = await hook_module_service.list_modules(enabled=enabled, scope=scope)
|
||
return {"code": 200, "data": {"total": len(modules), "modules": modules}}
|
||
|
||
|
||
@router.post("/modules")
|
||
async def create_module(req: ModuleUpsertRequest):
|
||
data = await hook_module_service.upsert_module(req.model_dump())
|
||
return {"code": 200, "data": data}
|
||
|
||
|
||
@router.get("/modules/{module_id}")
|
||
async def get_module(module_id: str):
|
||
item = await hook_module_service.get_module(module_id)
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="模块不存在")
|
||
return {"code": 200, "data": item}
|
||
|
||
|
||
@router.put("/modules/{module_id}")
|
||
async def update_module(module_id: str, req: ModuleUpsertRequest):
|
||
payload = req.model_dump()
|
||
payload["module_id"] = module_id
|
||
data = await hook_module_service.upsert_module(payload)
|
||
return {"code": 200, "data": data}
|
||
|
||
|
||
@router.delete("/modules/{module_id}")
|
||
async def delete_module(module_id: str):
|
||
ok = await hook_module_service.delete_module(module_id)
|
||
if not ok:
|
||
raise HTTPException(status_code=404, detail="模块不存在")
|
||
return {"code": 200, "data": {"module_id": module_id, "deleted": True}}
|
||
|
||
|
||
@router.put("/modules/{module_id}/scope")
|
||
async def set_module_scope(module_id: str, req: ScopeUpdateRequest):
|
||
item = await hook_module_service.set_scope(module_id, req.scopes)
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="模块不存在")
|
||
return {"code": 200, "data": {"module_id": module_id, "scopes": item.get("scopes", [])}}
|
||
|
||
|
||
@router.post("/modules/{module_id}/enable")
|
||
async def enable_module(module_id: str):
|
||
item = await hook_module_service.set_enabled(module_id, True)
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="模块不存在")
|
||
return {"code": 200, "data": {"module_id": module_id, "enabled": True, "affected_devices": item.get("device_count", 0)}}
|
||
|
||
|
||
@router.post("/modules/{module_id}/disable")
|
||
async def disable_module(module_id: str):
|
||
item = await hook_module_service.set_enabled(module_id, False)
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="模块不存在")
|
||
return {"code": 200, "data": {"module_id": module_id, "enabled": False}}
|
||
|
||
|
||
@router.get("/devices/{device_id}/modules")
|
||
async def get_device_modules(device_id: str):
|
||
probe = _probe_hook_ready(device_id)
|
||
await hook_module_service.update_device_probe(device_id, probe)
|
||
data = await hook_module_service.get_device_modules(device_id)
|
||
data["supports_hook"] = probe["supports_hook"]
|
||
data["frida_version"] = probe["frida_version"]
|
||
data["root_status"] = probe["root_status"]
|
||
data["probe_detail"] = probe["detail"]
|
||
return {"code": 200, "data": data}
|
||
|
||
|
||
@router.post("/devices/{device_id}/modules/reload")
|
||
async def reload_device_modules(device_id: str, req: ReloadDeviceModulesRequest):
|
||
result = await hook_module_service.reload_device_modules(device_id, req.module_ids, force=req.force)
|
||
return {"code": 200, "data": result}
|
||
|
||
|
||
@router.get("/devices/{device_id}/modules/{module_id}/logs")
|
||
async def get_device_module_logs(device_id: str, module_id: str):
|
||
logs = await hook_module_service.get_device_logs(device_id, module_id)
|
||
return {"code": 200, "data": {"device_id": device_id, "module_id": module_id, "logs": logs}}
|
||
|
||
|
||
@router.get("/scripts")
|
||
async def list_scripts():
|
||
scripts = await hook_module_service.list_scripts()
|
||
return {"code": 200, "data": {"total": len(scripts), "scripts": scripts}}
|
||
|
||
|
||
@router.post("/scripts")
|
||
async def upload_script(
|
||
file: UploadFile = File(...),
|
||
module_id: str = "wechat_hook_v1",
|
||
version: str = "1.0.0",
|
||
description: str = "",
|
||
):
|
||
raw = await file.read()
|
||
script_id = f"{module_id}_{version}"
|
||
saved = await hook_module_service.save_script(script_id, raw)
|
||
await hook_module_service.upsert_module(
|
||
{
|
||
"module_id": module_id,
|
||
"name": "微信Hook模块" if "wechat" in module_id else module_id,
|
||
"version": version,
|
||
"description": description or "Hook脚本上传",
|
||
"scopes": ["com.tencent.mm"] if "wechat" in module_id else [],
|
||
"capabilities": ["send_message", "get_messages", "get_contacts"] if "wechat" in module_id else [],
|
||
"min_frida_version": "16.0.0",
|
||
"enabled": True,
|
||
"script_id": script_id,
|
||
"script_url": saved["url"],
|
||
"script_hash": saved["hash"],
|
||
}
|
||
)
|
||
return {"code": 200, "data": saved}
|
||
|
||
|
||
@router.get("/scripts/{script_id}")
|
||
async def download_script(script_id: str):
|
||
path = await hook_module_service.get_script_path(script_id)
|
||
if not path:
|
||
raise HTTPException(status_code=404, detail="脚本不存在")
|
||
return FileResponse(path, media_type="application/javascript", filename=path.name)
|
||
|
||
|
||
@router.post("/scripts/{script_id}/deploy")
|
||
async def deploy_script(script_id: str, req: DeployScriptRequest):
|
||
result = await hook_module_service.deploy_script(script_id, req.device_ids, auto_reload=req.auto_reload)
|
||
for did in result["deployed"]:
|
||
await hook_module_service.add_device_log(did, script_id, f"deploy {script_id} ok")
|
||
return {"code": 200, "data": result}
|
||
|
||
|
||
@router.get("/hook/events")
|
||
async def list_hook_events(
|
||
event_type: Optional[str] = None,
|
||
device_id: Optional[str] = None,
|
||
platform: Optional[str] = None,
|
||
limit: int = 100,
|
||
):
|
||
events = await hook_module_service.list_events(
|
||
event_type=event_type,
|
||
device_id=device_id,
|
||
platform=platform,
|
||
limit=max(1, min(limit, 500)),
|
||
)
|
||
return {"code": 200, "data": {"total": len(events), "events": events}}
|
||
|
||
|
||
@router.post("/hook/events")
|
||
async def ingest_hook_event(payload: dict):
|
||
event = await hook_module_service.add_event(payload)
|
||
return {"code": 200, "data": event}
|
||
|
||
|
||
@router.websocket("/hook/events/stream")
|
||
async def hook_event_stream(websocket: WebSocket):
|
||
await websocket.accept()
|
||
await hook_module_service.attach_ws_client(websocket)
|
||
try:
|
||
while True:
|
||
# 订阅接口支持客户端心跳/控制消息,当前不做强校验
|
||
_ = await websocket.receive_text()
|
||
except WebSocketDisconnect:
|
||
await hook_module_service.detach_ws_client(websocket)
|
||
except Exception:
|
||
await hook_module_service.detach_ws_client(websocket)
|