""" 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 from services.ws_hub import ws_hub 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) if ws_hub.is_online(device_id): result["agent_reload"] = await ws_hub.send_command( device_id, { "type": "frida_reload", "data": {"module_ids": req.module_ids, "force": req.force}, }, timeout=30, ) else: result["agent_reload"] = {"code": 503, "message": "设备 WebSocket 不在线"} 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)