autool/utils_android/Manager/pcapdroid_manager.py
2026-06-17 19:44:18 +08:00

338 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- encoding=utf8 -*-
"""
PCAPDroid Manager - ADB 命令行控制版
通过 ADB 命令控制 PCAPDroid 的启停和状态监控,不依赖 Airtest/Poco。
"""
import time
import logging
import csv
import json
import base64
from collections import defaultdict
from pathlib import Path
logger = logging.getLogger(__name__)
class AppBlocker:
"""应用阻塞器:根据包名或标签名导入域名屏蔽规则到 PCAPDroid blocklist
规则库由两个 CSV 文件组成:
- app_list: 包名 -> 应用名/标签映射
- url_lib: 应用名 -> 域名列表
"""
BLOCKLIST_ACTIVITY = (
"com.emanuelef.remote_capture/"
"com.emanuelef.remote_capture.activities.BlocklistCtrl"
)
def __init__(self, adb_helper, app_list_file=None, url_lib_file=None):
"""
Args:
adb_helper: ADBHelper 实例
app_list_file: 应用列表 CSV 路径(包含 package_name, app_name, final_tag_id 列)
url_lib_file: URL 库 CSV 路径(包含 app, url 列)
"""
self.adb_helper = adb_helper
self.app_list_file = app_list_file
self.url_lib_file = url_lib_file
self._apps = None
self._urls = None
# ------------------------------------------------------------------
# 规则加载
# ------------------------------------------------------------------
def _load_app_mapping(self):
if self._apps is not None:
return self._apps
self._apps = {}
if not self.app_list_file or not Path(self.app_list_file).exists():
logger.warning(f"[AppBlock] 应用列表文件不存在: {self.app_list_file}")
return self._apps
try:
with open(self.app_list_file, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if row.get("status", "Active") != "Active":
continue
pkg_names = row.get("package_name", "")
if pkg_names:
for pkg in pkg_names.split(","):
pkg = pkg.strip().strip('"')
if pkg:
self._apps[pkg] = {
"app_name": row.get("app_name", "Unknown"),
"category": row.get("final_tag_id", "Unknown"),
}
logger.info(f"[AppBlock] 已加载应用映射: {len(self._apps)}")
except Exception as e:
logger.error(f"[AppBlock] 加载应用列表失败: {e}")
return self._apps
def _load_url_lib(self):
if self._urls is not None:
return self._urls
self._urls = {}
if not self.url_lib_file or not Path(self.url_lib_file).exists():
logger.warning(f"[AppBlock] URL 库文件不存在: {self.url_lib_file}")
return self._urls
try:
with open(self.url_lib_file, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
app_name = row.get("app", "")
url = row.get("url", "")
if app_name and url:
self._urls.setdefault(app_name, []).append(url)
logger.info(f"[AppBlock] 已加载 URL 库: {len(self._urls)} 个应用")
except Exception as e:
logger.error(f"[AppBlock] 加载 URL 库失败: {e}")
return self._urls
# ------------------------------------------------------------------
# 域名查找
# ------------------------------------------------------------------
def find_domains_by_package(self, package_name):
apps = self._load_app_mapping()
urls = self._load_url_lib()
if package_name not in apps:
return [], None
app_info = apps[package_name]
app_name = app_info["app_name"]
domains = urls.get(app_name, [])
return domains, app_info
def find_domains_by_tag(self, tag_name):
urls = self._load_url_lib()
if tag_name in urls:
return urls[tag_name], tag_name
for app_name in urls:
if app_name.lower() == tag_name.lower():
return urls[app_name], app_name
return [], None
# ------------------------------------------------------------------
# 域名简化
# ------------------------------------------------------------------
@staticmethod
def _extract_main_domain(domain):
parts = domain.lower().split(".")
if len(parts) >= 2:
return ".".join(parts[-2:])
return domain
@staticmethod
def simplify_domains(domains):
if not domains:
return []
groups = defaultdict(list)
for d in domains:
groups[AppBlocker._extract_main_domain(d)].append(d)
simplified = []
for main_domain, sub_domains in groups.items():
if len(sub_domains) >= 2:
simplified.append(main_domain)
else:
simplified.append(sub_domains[0])
return sorted(set(simplified))
# ------------------------------------------------------------------
# Blocklist 导入/清除
# ------------------------------------------------------------------
def import_blocklist(self, domains, mode="set"):
"""导入域名规则到 PCAPDroid blocklist
Args:
domains: 域名列表
mode: "set" (替换) 或 "append" (追加)
Returns:
bool: 是否成功
"""
if not domains:
logger.warning("[AppBlock] 无域名可导入")
return False
original_count = len(domains)
simplified = self.simplify_domains(domains)
logger.info(f"[AppBlock] 域名简化: {original_count} -> {len(simplified)}")
rules = {"rules": [{"type": "HOST", "value": d} for d in simplified]}
json_str = json.dumps(rules)
json_b64 = base64.b64encode(json_str.encode("utf-8")).decode("ascii")
action = "set_rules" if mode == "set" else "append_rules"
logger.info(f"[AppBlock] 正在导入 {len(simplified)} 条规则 (mode={mode})")
output = self.adb_helper.run_cmd([
"shell", "am", "start", "-W",
"-n", self.BLOCKLIST_ACTIVITY,
"-e", "action", action,
"-e", "rules_json_base64", json_b64,
])
logger.info(f"[AppBlock] 导入输出: {output}")
time.sleep(1)
return True
def clear_blocklist(self):
"""清除 PCAPDroid blocklist 中的所有规则"""
logger.info("[AppBlock] 正在清除 blocklist")
try:
output = self.adb_helper.run_cmd([
"shell", "am", "start", "-W",
"-n", self.BLOCKLIST_ACTIVITY,
"-e", "action", "clear",
])
logger.info(f"[AppBlock] 清除输出: {output}")
time.sleep(1)
return True
except Exception as e:
logger.warning(f"[AppBlock] 清除 blocklist 失败: {e}")
return False
def import_for_package(self, package_name, mode="set", tag=None):
"""根据包名(或标签名)查找并导入 blocklist 规则
Args:
package_name: 应用包名
mode: "set""append"
tag: 可选的标签名,优先使用标签名查找
Returns:
bool: 是否成功
"""
if tag:
domains, matched = self.find_domains_by_tag(tag)
if matched:
logger.info(f"[AppBlock] 通过标签 '{matched}' 找到 {len(domains)} 个域名")
return self.import_blocklist(domains, mode=mode)
logger.warning(f"[AppBlock] 标签 '{tag}' 未找到,尝试通过包名查找")
domains, app_info = self.find_domains_by_package(package_name)
if app_info:
logger.info(
f"[AppBlock] 包名 '{package_name}' 对应应用: {app_info['app_name']}"
)
return self.import_blocklist(domains, mode=mode)
logger.warning(f"[AppBlock] 包名 '{package_name}' 在规则库中未找到")
return False
class PCAPManager:
"""负责 PCAPdroid 应用的控制、配置与状态监控ADB 命令版)
使用 adb_helper.run_cmd() 执行所有 ADB 命令,确保异常捕获链路完整。
注意:不使用 adb_helper.shell(),因为底层 ADB.shell() 会对参数做 shlex.quote()
导致 am start 的 -e 参数被错误引用。直接通过 run_cmd(["shell", ...]) 绕过。
"""
PKG_NAME = "com.emanuelef.remote_capture"
CAPTURE_CTRL = f"{PKG_NAME}/{PKG_NAME}.activities.CaptureCtrl"
def __init__(self, adb_helper, *, root_capture=True, app_blocker=None):
"""
Args:
adb_helper: ADBHelper 实例,用于执行 ADB 命令
root_capture: 是否使用 root 模式抓包(开启应用阻塞时必须为 False
app_blocker: AppBlocker 实例(可选)
"""
self.adb_helper = adb_helper
self.root_capture = bool(root_capture)
self.app_blocker = app_blocker
# ------------------------------------------------------------------
# Blocklist 快捷方法
# ------------------------------------------------------------------
def import_app_blocklist(self, package_name, mode="set", tag=None):
"""导入应用阻塞规则(委托给 AppBlocker"""
if self.app_blocker:
return self.app_blocker.import_for_package(package_name, mode=mode, tag=tag)
logger.warning("[PCAP] AppBlocker 未初始化,跳过导入 blocklist")
return False
def clear_app_blocklist(self):
"""清除应用阻塞规则(委托给 AppBlocker"""
if self.app_blocker:
return self.app_blocker.clear_blocklist()
return False
# ------------------------------------------------------------------
# 抓包控制
# ------------------------------------------------------------------
def start_capture(self, package_name, pcap_dump_mode="pcap_file", max_retries=3):
"""开启抓包流程
Args:
package_name: 目标应用包名
pcap_dump_mode: 抓包数据存储模式,默认 pcap_file
max_retries: 最大重试次数
Returns:
bool: 是否成功开启抓包
"""
print(f"[PCAP] 准备开启抓包: {package_name} (root={self.root_capture})")
# 如果已经在运行,先停止
if self.is_running():
print("[PCAP] 检测到抓包正在运行,先停止...")
self.stop_capture()
time.sleep(2)
for attempt in range(max_retries):
# 通过 CaptureCtrl Activity 启动抓包
print(f"[PCAP] 执行启动命令 (尝试 {attempt + 1}/{max_retries})")
output = self.adb_helper.run_cmd([
"shell", "am", "start", "-n", self.CAPTURE_CTRL,
"-e", "action", "start",
"-e", "pcap_dump_mode", pcap_dump_mode,
"-e", "app_filter", package_name,
"-e", "root_capture", "true" if self.root_capture else "false",
])
logger.info(f"[PCAP] am start 输出: {output}")
# 等待抓包启动PCAPDroid 需要约 15 秒才能在通知栏显示状态)
time.sleep(15)
# 按 Home 键将 PCAPDroid 推到后台
self.adb_helper.run_cmd(["shell", "input", "keyevent", "3"])
time.sleep(1)
# 验证是否成功启动
if self.is_running():
print("[PCAP] 抓包已成功启动")
return True
print(f"[PCAP] 第 {attempt + 1} 次启动未成功,重试...")
time.sleep(2)
print("[PCAP] 抓包启动失败")
return False
def stop_capture(self):
"""停止抓包"""
print("[PCAP] 停止 PCAPdroid")
self.adb_helper.run_cmd([
"shell", "am", "start", "-n", self.CAPTURE_CTRL,
"-e", "action", "stop",
])
time.sleep(1)
def is_running(self):
"""通过通知栏检查抓包是否正在运行
Returns:
bool: 抓包是否正在运行
"""
try:
output = self.adb_helper.run_cmd([
"shell", "dumpsys", "notification", "--noredact"
])
# 在整个通知输出中搜索 PCAPDroid 的运行通知
if self.PKG_NAME in output and "Capture running" in output:
logger.info("[PCAP] 状态检测: 抓包正在运行")
return True
logger.info("[PCAP] 状态检测: 抓包未运行")
except Exception as e:
logger.warning(f"[PCAP] 检查运行状态失败: {e}")
return False