# -*- 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