From d79017e2d877265c1a73af69e06bd59fdd89451c Mon Sep 17 00:00:00 2001 From: admin9webs Date: Fri, 10 Jul 2026 16:11:57 +0800 Subject: [PATCH] Add browser-automation/camoufox-google-2fa-login --- .../camoufox-google-2fa-login/SKILL.md | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 skills/browser-automation/camoufox-google-2fa-login/SKILL.md diff --git a/skills/browser-automation/camoufox-google-2fa-login/SKILL.md b/skills/browser-automation/camoufox-google-2fa-login/SKILL.md new file mode 100644 index 0000000..13c8624 --- /dev/null +++ b/skills/browser-automation/camoufox-google-2fa-login/SKILL.md @@ -0,0 +1,257 @@ +--- +name: camoufox-google-2fa-login +title: CamouFox Google 2FA登录 +description: 使用CamouFox反检测浏览器登录Google账号,处理2FA短信验证码。关键:验证码与session绑定,必须保存storage_state在同一会话输入。 +category: browser-automation +tags: [camoufox, google, 2fa, login, anti-detection] +--- + +# CamouFox Google 2FA 登录 + +## 核心问题 + +Google 2FA短信验证码是**session-bound**的。每次新的登录会话(新browser context)会自动使之前的验证码失效。因此: + +- **错误做法1**:登录到2FA页 → 告诉用户等待 → 用户给码 → 开新session重新登录 → 输入验证码 → "验证码错误"(码已被新session失效) +- **错误做法2**:登录到2FA页 → `storage_state`保存 → 用户给码 → 新session用storage_state恢复 → Google返回400错误(TL参数已失效,storage_state无法恢复2FA中间状态) +- **正确做法**:登录到2FA页 → **后台脚本保持浏览器会话存活** → 轮询文件等待验证码 → 用户收到短信后写入文件 → **同一会话**内读取并输入验证码 + +## 依赖 + +```bash +pip install camoufox +playwright install # camoufox依赖playwright +``` + +## 完整流程 + +### 阶段1:登录到2FA验证码页面 + +```python +from camoufox.sync_api import Camoufox +import time, json + +email = "my9commerce@gmail.com" +password = "Bingo2019Cc9" +STATE_FILE = "/tmp/gmail_2fa_state.json" + +with Camoufox(headless=True, geoip=True, humanize=True) as browser: + page = browser.new_page() + + # Step 1: Email + page.goto("https://accounts.google.com/signin", timeout=30000) + page.wait_for_load_state("networkidle", timeout=15000) + time.sleep(2) + page.query_selector("input[type=email]").fill(email) + time.sleep(1) + page.query_selector("#identifierNext").click() + time.sleep(5) + + # Step 2: Password + page.query_selector("input[type=password]").fill(password) + time.sleep(1) + page.query_selector("#passwordNext").click() + time.sleep(6) + + # Step 3: 2FA - click SMS option + page.evaluate('''() => { + const items = document.querySelectorAll("[role=link]"); + for (const item of items) { + const t = (item.innerText || ""); + if (t.includes("验证码") || t.includes("verification code") || t.includes("••47")) { + item.click(); return; + } + } + }''') + time.sleep(6) + + # Verify we're on code entry page + body = page.evaluate("document.body.innerText") + if "输入验证码" in body or "Enter the code" in body: + # 保存完整浏览器状态(关键!) + page.context.storage_state(path=STATE_FILE) + print("SMS code sent! Waiting for user to provide the code.") + print(f"State saved to {STATE_FILE}") + else: + print("Failed to reach code entry page") +``` + +### 正确做法:后台脚本保持会话 + 文件轮询 + +**关键**:必须在同一个浏览器会话内输入验证码。用后台脚本保持CamouFox进程存活,通过文件信号机制传递验证码。 + +#### 后台登录脚本(gmail_login_wait.py) + +```python +#!/usr/bin/env python3 +import json, time, os, sys +from camoufox.sync_api import Camoufox + +email = "my9commerce@gmail.com" +password = "YOUR_PASSWORD" +CODE_FILE = "/tmp/gmail_code.txt" +SIGNAL_FILE = "/tmp/gmail_signal.txt" + +# Clean old files +for f in [CODE_FILE, SIGNAL_FILE]: + if os.path.exists(f): os.remove(f) + +print("STARTING", flush=True) + +with Camoufox(headless=True, geoip=True, humanize=True) as browser: + page = browser.new_page() + + # Step 1: Email + page.goto("https://accounts.google.com/signin", timeout=30000) + page.wait_for_load_state("networkidle", timeout=15000) + time.sleep(2) + page.query_selector("input[type=email]").fill(email) + time.sleep(1) + page.query_selector("#identifierNext").click() + time.sleep(5) + + # Step 2: Password + page.query_selector("input[type=password]").fill(password) + time.sleep(1) + page.query_selector("#passwordNext").click() + time.sleep(6) + + # Step 3: 2FA - click SMS option + page.evaluate('''() => { + const items = document.querySelectorAll("[role=link]"); + for (const item of items) { + const t = (item.innerText || ""); + if (t.includes("验证码") || t.includes("verification code") || t.includes("••47")) { + item.click(); return; + } + } + }''') + time.sleep(6) + + # Verify on code entry page + body = page.evaluate("document.body.innerText") + if "输入验证码" not in body and "Enter the code" not in body: + print(f"NOT_ON_CODE_PAGE", flush=True) + sys.exit(1) + + # Signal ready - SMS has been sent + print("WAITING_FOR_CODE", flush=True) + with open(SIGNAL_FILE, "w") as f: + f.write("READY") + + # Poll for code file (max 120 seconds) + code = None + for i in range(120): + if os.path.exists(CODE_FILE): + with open(CODE_FILE, "r") as f: + code = f.read().strip() + if len(code) == 6 and code.isdigit(): + print(f"GOT_CODE: {code}", flush=True) + break + time.sleep(1) + else: + print("TIMEOUT", flush=True) + sys.exit(1) + + # Step 4: Enter code IN SAME SESSION + pin = page.query_selector("#idvPin") + if pin: + pin.fill(code) + time.sleep(1) + + page.evaluate('''() => { + const btns = document.querySelectorAll("button"); + for (const b of btns) { + const t = (b.innerText || "").trim(); + if (t === "下一步" || t === "Next") { b.click(); return; } + } + }''') + time.sleep(10) + + body2 = page.evaluate("document.body.innerText") + if "错误" in body2 or "wrong" in body2.lower(): + print("CODE_REJECTED", flush=True) + sys.exit(1) + + page.goto("https://mail.google.com", timeout=20000) + time.sleep(5) + if "mail.google.com" in page.url: + print("LOGIN_SUCCESS", flush=True) + cookies = page.context.cookies() + with open("/home/ubuntu/.hermes/cookies/gmail_cookies.json", "w") as f: + json.dump(cookies, f) + print("COOKIES_SAVED", flush=True) + else: + print(f"LOGIN_FAILED: {page.url}", flush=True) +``` + +#### 启动与交互流程 + +```bash +# 1. 后台启动脚本(注意 -u 参数确保无缓冲输出) +/home/ubuntu/.hermes/hermes-agent/venv/bin/python3 -u gmail_login_wait.py & + +# 2. 等待信号文件 +while [ ! -f /tmp/gmail_signal.txt ]; do sleep 2; done +echo "Ready for code" + +# 3. 用户确认收到短信后,写入验证码 +echo "123456" > /tmp/gmail_code.txt + +# 4. 检查结果 +# 脚本会输出 LOGIN_SUCCESS 或 CODE_REJECTED +``` + +**重要时序**:脚本启动后Google会发送新验证码。用户必须等**这个脚本触发的**短信,而不是之前任何会话的验证码。 + +1. **必须用CamouFox**:普通headless Chromium会被Google拦截,重定向到`/signin/rejected`("此浏览器或应用可能不安全")。CamouFox的指纹伪装+geoip+humanize能绕过检测。 + +2. **2FA验证码是session-bound**:新登录会话会使旧验证码失效。必须在同一session内输入验证码,或用`storage_state`保存/恢复session。 + +3. **2FA选择页面的点击**:Google 2FA选择页面用`[role="link"]`元素,不是按钮。需要通过文本内容("验证码"或手机尾号)定位。 + +4. **PIN输入框**:`#idvPin`,type为`tel`,name为`Pin`。 + +5. **storage_state无法恢复2FA中间状态**:实测发现用storage_state恢复session后访问2FA页面返回400错误。Google的TL参数是临时的、一次性的。**必须保持原始浏览器进程存活**,不能关掉后重新打开。 + +6. **等待时间**:Google的页面切换需要较长等待(5-6秒),网络不稳定时可能需要更长。 + +## 常见问题 + +| 问题 | 原因 | 解决方案 | +|------|------|----------| +| "此浏览器或应用可能不安全" | 普通Chromium被Google反自动化检测 | 使用CamouFox(headless=True, geoip=True, humanize=True) | +| "验证码错误" | 验证码来自不同的登录session | 用storage_state保存/恢复session,在同一session输入 | +| 2FA选择页面点击无反应 | 点击了外层容器而非内部链接 | 用`[role="link"]`定位,通过innerText匹配 | +| 页面停留在challenge/selection | SMS选项点击未触发跳转 | 确认JS evaluate中click生效,增加等待时间 | +| storage_state恢复后返回400错误 | Google TL参数是临时的一次性的,storage_state无法恢复2FA中间状态 | **不要用storage_state恢复**,用后台脚本保持浏览器进程存活,通过文件轮询传递验证码 | + +## CamouFox启动参数 + +```python +with Camoufox( + headless=True, # 无头模式(服务器环境必须) + geoip=True, # 自动匹配IP地理位置的指纹 + humanize=True, # 模拟人类操作模式 +) as browser: + page = browser.new_page() +``` + +## Python路径 + +系统上CamouFox安装在Hermes venv中: +``` +/home/ubuntu/.hermes/hermes-agent/venv/bin/python3 +``` + +注意:`/root/`路径可能权限不足,用`/home/ubuntu/`路径。 + +## 经验教训(踩坑记录) + +1. **验证码时序陷阱**:用户给的是上一轮会话的验证码,但新一轮登录已经使旧码失效。每次新CamouFox会话 = 新登录 = 新验证码。**必须等用户确认收到当前会话的短信后再输入**。 + +2. **execute_code无法保持会话**:execute_code每次都是新进程,无法保持浏览器会话。必须用terminal(background=true)启动脚本,通过文件信号机制(SIGNAL_FILE/CODE_FILE)协调。 + +3. **脚本输出需要flush**:Python后台脚本必须用`print(..., flush=True)`或`python3 -u`参数,否则stdout缓冲导致无法实时看到输出。 + +4. **CamouFox启动较慢**:从启动到2FA页面约需30-40秒,轮询间隔不要太短。