feat: SDK 更新 | agent/hook/hawk/soul、Android 多 Fragment、Hook 模块与连接路由、开发文档
Made-with: Cursor
This commit is contained in:
350
sdk/app/services/hook_module_service.py
Normal file
350
sdk/app/services/hook_module_service.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Hook 模块管理服务
|
||||
|
||||
实现能力:
|
||||
1. 模块管理(增删改查/启停/scope)
|
||||
2. 脚本管理(保存/下载/部署)
|
||||
3. 设备模块状态(加载/重载/日志)
|
||||
4. Hook 事件总线(历史 + 实时订阅)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
class HookModuleService:
|
||||
def __init__(self) -> None:
|
||||
app_dir = Path(__file__).resolve().parent.parent
|
||||
self.data_dir = app_dir / "data" / "hook"
|
||||
self.scripts_dir = self.data_dir / "scripts"
|
||||
self.modules_file = self.data_dir / "modules.json"
|
||||
self.events_file = self.data_dir / "events.jsonl"
|
||||
self.device_state_file = self.data_dir / "device_modules.json"
|
||||
self._lock = asyncio.Lock()
|
||||
self._event_clients: Set[Any] = set()
|
||||
self._ensure_store()
|
||||
|
||||
def _ensure_store(self) -> None:
|
||||
self.scripts_dir.mkdir(parents=True, exist_ok=True)
|
||||
if not self.modules_file.exists():
|
||||
self.modules_file.write_text("{}", encoding="utf-8")
|
||||
if not self.device_state_file.exists():
|
||||
self.device_state_file.write_text("{}", encoding="utf-8")
|
||||
if not self.events_file.exists():
|
||||
self.events_file.write_text("", encoding="utf-8")
|
||||
|
||||
def _read_json(self, path: Path, default: Any) -> Any:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def _write_json(self, path: Path, data: Any) -> None:
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
async def list_modules(self, enabled: Optional[bool] = None, scope: Optional[str] = None) -> List[dict]:
|
||||
async with self._lock:
|
||||
modules = self._read_json(self.modules_file, {})
|
||||
result = list(modules.values())
|
||||
if enabled is not None:
|
||||
result = [m for m in result if bool(m.get("enabled")) == enabled]
|
||||
if scope:
|
||||
result = [m for m in result if scope in (m.get("scopes") or [])]
|
||||
return sorted(result, key=lambda x: x.get("updated_at", ""), reverse=True)
|
||||
|
||||
async def get_module(self, module_id: str) -> Optional[dict]:
|
||||
async with self._lock:
|
||||
modules = self._read_json(self.modules_file, {})
|
||||
return modules.get(module_id)
|
||||
|
||||
async def upsert_module(self, payload: dict) -> dict:
|
||||
module_id = payload["module_id"]
|
||||
now = _now_iso()
|
||||
script_content = payload.pop("script_content", None)
|
||||
script_url = payload.get("script_url")
|
||||
script_hash = payload.get("script_hash")
|
||||
if script_content and not script_url:
|
||||
decoded = base64.b64decode(script_content)
|
||||
script_name = f"{module_id}.js"
|
||||
target = self.scripts_dir / script_name
|
||||
target.write_bytes(decoded)
|
||||
digest = hashlib.sha256(decoded).hexdigest()
|
||||
script_url = f"/api/v3/scripts/{module_id}"
|
||||
script_hash = f"sha256:{digest}"
|
||||
|
||||
async with self._lock:
|
||||
modules = self._read_json(self.modules_file, {})
|
||||
old = modules.get(module_id, {})
|
||||
data = {
|
||||
"module_id": module_id,
|
||||
"name": payload.get("name", old.get("name", module_id)),
|
||||
"version": payload.get("version", old.get("version", "1.0.0")),
|
||||
"description": payload.get("description", old.get("description", "")),
|
||||
"enabled": bool(payload.get("enabled", old.get("enabled", True))),
|
||||
"scopes": payload.get("scopes", old.get("scopes", ["com.tencent.mm"])),
|
||||
"capabilities": payload.get("capabilities", old.get("capabilities", [])),
|
||||
"min_frida_version": payload.get("min_frida_version", old.get("min_frida_version", "16.0.0")),
|
||||
"script_url": script_url or old.get("script_url"),
|
||||
"script_hash": script_hash or old.get("script_hash"),
|
||||
"script_id": payload.get("script_id", old.get("script_id", module_id)),
|
||||
"device_count": old.get("device_count", 0),
|
||||
"error_count": old.get("error_count", 0),
|
||||
"created_at": old.get("created_at", now),
|
||||
"updated_at": now,
|
||||
}
|
||||
modules[module_id] = data
|
||||
self._write_json(self.modules_file, modules)
|
||||
return data
|
||||
|
||||
async def delete_module(self, module_id: str) -> bool:
|
||||
async with self._lock:
|
||||
modules = self._read_json(self.modules_file, {})
|
||||
if module_id not in modules:
|
||||
return False
|
||||
del modules[module_id]
|
||||
self._write_json(self.modules_file, modules)
|
||||
return True
|
||||
|
||||
async def set_scope(self, module_id: str, scopes: List[str]) -> Optional[dict]:
|
||||
async with self._lock:
|
||||
modules = self._read_json(self.modules_file, {})
|
||||
item = modules.get(module_id)
|
||||
if not item:
|
||||
return None
|
||||
item["scopes"] = scopes
|
||||
item["updated_at"] = _now_iso()
|
||||
modules[module_id] = item
|
||||
self._write_json(self.modules_file, modules)
|
||||
return item
|
||||
|
||||
async def set_enabled(self, module_id: str, enabled: bool) -> Optional[dict]:
|
||||
async with self._lock:
|
||||
modules = self._read_json(self.modules_file, {})
|
||||
item = modules.get(module_id)
|
||||
if not item:
|
||||
return None
|
||||
item["enabled"] = enabled
|
||||
item["updated_at"] = _now_iso()
|
||||
modules[module_id] = item
|
||||
self._write_json(self.modules_file, modules)
|
||||
return item
|
||||
|
||||
async def list_scripts(self) -> List[dict]:
|
||||
out: List[dict] = []
|
||||
for p in sorted(self.scripts_dir.glob("*.js"), key=lambda x: x.stat().st_mtime, reverse=True):
|
||||
raw = p.read_bytes()
|
||||
out.append(
|
||||
{
|
||||
"script_id": p.stem,
|
||||
"filename": p.name,
|
||||
"size": len(raw),
|
||||
"hash": f"sha256:{hashlib.sha256(raw).hexdigest()}",
|
||||
"url": f"/api/v3/scripts/{p.stem}",
|
||||
"updated_at": datetime.fromtimestamp(p.stat().st_mtime, tz=timezone.utc).isoformat(),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
async def save_script(self, script_id: str, content: bytes) -> dict:
|
||||
target = self.scripts_dir / f"{script_id}.js"
|
||||
target.write_bytes(content)
|
||||
digest = hashlib.sha256(content).hexdigest()
|
||||
return {
|
||||
"script_id": script_id,
|
||||
"url": f"/api/v3/scripts/{script_id}",
|
||||
"hash": f"sha256:{digest}",
|
||||
"size": len(content),
|
||||
}
|
||||
|
||||
async def get_script_path(self, script_id: str) -> Optional[Path]:
|
||||
target = self.scripts_dir / f"{script_id}.js"
|
||||
return target if target.exists() else None
|
||||
|
||||
async def get_device_modules(self, device_id: str) -> dict:
|
||||
async with self._lock:
|
||||
state = self._read_json(self.device_state_file, {})
|
||||
item = state.get(device_id, {})
|
||||
modules = item.get("modules", [])
|
||||
return {
|
||||
"device_id": device_id,
|
||||
"supports_hook": item.get("supports_hook", False),
|
||||
"frida_version": item.get("frida_version", ""),
|
||||
"hook_framework": item.get("hook_framework", "frida-server"),
|
||||
"root_status": item.get("root_status", False),
|
||||
"modules": modules,
|
||||
}
|
||||
|
||||
async def update_device_probe(self, device_id: str, probe: dict) -> None:
|
||||
async with self._lock:
|
||||
state = self._read_json(self.device_state_file, {})
|
||||
item = state.get(device_id, {})
|
||||
item.update(
|
||||
{
|
||||
"supports_hook": probe.get("supports_hook", False),
|
||||
"frida_version": probe.get("frida_version", ""),
|
||||
"root_status": probe.get("root_status", False),
|
||||
"hook_framework": "frida-server",
|
||||
"updated_at": _now_iso(),
|
||||
}
|
||||
)
|
||||
item.setdefault("modules", [])
|
||||
state[device_id] = item
|
||||
self._write_json(self.device_state_file, state)
|
||||
|
||||
async def deploy_script(self, script_id: str, device_ids: List[str], auto_reload: bool = True) -> dict:
|
||||
async with self._lock:
|
||||
state = self._read_json(self.device_state_file, {})
|
||||
deployed: List[str] = []
|
||||
failed: List[dict] = []
|
||||
reloaded: List[str] = []
|
||||
now = _now_iso()
|
||||
for did in device_ids:
|
||||
item = state.setdefault(
|
||||
did,
|
||||
{"supports_hook": False, "frida_version": "", "root_status": False, "modules": []},
|
||||
)
|
||||
modules = item.setdefault("modules", [])
|
||||
exists = None
|
||||
for m in modules:
|
||||
if m.get("module_id") == script_id:
|
||||
exists = m
|
||||
break
|
||||
if exists:
|
||||
exists["status"] = "loaded"
|
||||
exists["loaded_at"] = now
|
||||
else:
|
||||
modules.append(
|
||||
{
|
||||
"module_id": script_id,
|
||||
"version": "1.0.0",
|
||||
"status": "loaded",
|
||||
"loaded_at": now,
|
||||
"target_process": "com.tencent.mm",
|
||||
"target_pid": 0,
|
||||
"rpc_methods": [],
|
||||
"last_error": None,
|
||||
"events_today": 0,
|
||||
}
|
||||
)
|
||||
deployed.append(did)
|
||||
if auto_reload:
|
||||
reloaded.append(did)
|
||||
self._write_json(self.device_state_file, state)
|
||||
return {"deployed": deployed, "failed": failed, "reloaded": reloaded}
|
||||
|
||||
async def reload_device_modules(self, device_id: str, module_ids: List[str], force: bool = False) -> dict:
|
||||
async with self._lock:
|
||||
state = self._read_json(self.device_state_file, {})
|
||||
item = state.get(device_id)
|
||||
if not item:
|
||||
return {"reloaded": [], "failed": module_ids}
|
||||
mods = item.get("modules", [])
|
||||
found, failed = [], []
|
||||
now = _now_iso()
|
||||
for mid in module_ids:
|
||||
hit = None
|
||||
for m in mods:
|
||||
if m.get("module_id") == mid:
|
||||
hit = m
|
||||
break
|
||||
if hit:
|
||||
hit["status"] = "loaded"
|
||||
hit["loaded_at"] = now
|
||||
if force:
|
||||
hit["last_error"] = None
|
||||
found.append(mid)
|
||||
else:
|
||||
failed.append(mid)
|
||||
item["modules"] = mods
|
||||
state[device_id] = item
|
||||
self._write_json(self.device_state_file, state)
|
||||
return {"reloaded": found, "failed": failed}
|
||||
|
||||
async def add_device_log(self, device_id: str, module_id: str, line: str) -> None:
|
||||
async with self._lock:
|
||||
state = self._read_json(self.device_state_file, {})
|
||||
item = state.setdefault(device_id, {"modules": []})
|
||||
logs = item.setdefault("logs", {})
|
||||
bucket = logs.setdefault(module_id, [])
|
||||
bucket.append({"ts": _now_iso(), "line": line})
|
||||
if len(bucket) > 300:
|
||||
logs[module_id] = bucket[-300:]
|
||||
state[device_id] = item
|
||||
self._write_json(self.device_state_file, state)
|
||||
|
||||
async def get_device_logs(self, device_id: str, module_id: str) -> List[dict]:
|
||||
async with self._lock:
|
||||
state = self._read_json(self.device_state_file, {})
|
||||
item = state.get(device_id, {})
|
||||
logs = item.get("logs", {}).get(module_id, [])
|
||||
return logs[-200:]
|
||||
|
||||
async def add_event(self, event: dict) -> dict:
|
||||
data = {"event_id": f"evt_{int(datetime.now().timestamp()*1000)}", **event}
|
||||
if "timestamp" not in data:
|
||||
data["timestamp"] = _now_iso()
|
||||
async with self._lock:
|
||||
with self.events_file.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(data, ensure_ascii=False) + "\n")
|
||||
await self._broadcast_event({"type": "hook_event", "data": data})
|
||||
return data
|
||||
|
||||
async def list_events(
|
||||
self,
|
||||
event_type: Optional[str] = None,
|
||||
device_id: Optional[str] = None,
|
||||
platform: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
) -> List[dict]:
|
||||
events: List[dict] = []
|
||||
if not self.events_file.exists():
|
||||
return events
|
||||
lines = self.events_file.read_text(encoding="utf-8").splitlines()[-2000:]
|
||||
for line in reversed(lines):
|
||||
if len(events) >= limit:
|
||||
break
|
||||
try:
|
||||
item = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if event_type and item.get("event_type") != event_type:
|
||||
continue
|
||||
if device_id and item.get("device_id") != device_id:
|
||||
continue
|
||||
if platform and item.get("platform") != platform:
|
||||
continue
|
||||
events.append(item)
|
||||
return events
|
||||
|
||||
async def attach_ws_client(self, ws: Any) -> None:
|
||||
self._event_clients.add(ws)
|
||||
|
||||
async def detach_ws_client(self, ws: Any) -> None:
|
||||
self._event_clients.discard(ws)
|
||||
|
||||
async def _broadcast_event(self, payload: dict) -> None:
|
||||
if not self._event_clients:
|
||||
return
|
||||
dead = []
|
||||
for ws in list(self._event_clients):
|
||||
try:
|
||||
await ws.send_json(payload)
|
||||
except Exception:
|
||||
dead.append(ws)
|
||||
for ws in dead:
|
||||
self._event_clients.discard(ws)
|
||||
|
||||
|
||||
hook_module_service = HookModuleService()
|
||||
Reference in New Issue
Block a user