From bac7113c0c57cd6a95eb343d0e2bafdd0c3a1f10 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sat, 11 Apr 2026 01:31:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20OAuth=20=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E5=9C=B0=E5=9D=80=E9=AA=8C=E8=AF=81=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E4=BB=85=E6=8E=A5=E5=8F=97=E6=9C=89=E6=95=88=E7=9A=84?= =?UTF-8?q?=20localhost=20=E5=9B=9E=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++++++ background.js | 68 ++++++++++++++++++++++++++------------------ content/vps-panel.js | 31 ++++++++++++++++++-- 3 files changed, 86 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 85d3a23..2d456d0 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,12 @@ Step 3 使用的注册邮箱。 ### Step 8: Manual OAuth Confirm +严格回调捕获规则: + +- 步骤 8 现在只接受 `http(s)://localhost:/auth/callback?code=...&state=...` +- 监听范围只限于当前 OAuth 认证标签页的主 frame 跳转 +- 普通 `localhost` 页面,包括本地部署的 CPA 面板,不会再被误判为回调地址 + 虽然按钮名称还是 `Manual OAuth Confirm`,但当前代码已经做了自动尝试: - 在授权页定位“继续”按钮 @@ -259,6 +265,11 @@ Step 3 使用的注册邮箱。 ### Step 9: CPA Verify +校验规则: + +- 步骤 9 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的 `localhostUrl` +- 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径 + 回到 CPA 面板: - 自动填写 localhost 回调地址 @@ -381,6 +392,12 @@ sidepanel/ 侧边栏 UI ### 5. Step 8 失败时重点检查 +补充检查项: + +- 确认回调路径仍然是 `/auth/callback` +- 确认回调 query 里仍然同时包含 `code` 和 `state` +- 如果 CPA 部署在 `localhost`,确认当前看到的页面是真实 OAuth 回调,而不是 CPA 面板自身页面 + - OAuth 同意页 DOM 是否变化 - “继续”按钮是否变成了别的文案 - localhost 回调是否真的触发 diff --git a/background.js b/background.js index 747da50..6599841 100644 --- a/background.js +++ b/background.js @@ -229,16 +229,23 @@ function is163MailHost(hostname = '') { || hostname === 'webmail.vip.163.com'; } -function buildLocalhostCleanupPrefix(rawUrl) { +function isLocalhostOAuthCallbackUrl(rawUrl) { const parsed = parseUrlSafely(rawUrl); - if (!parsed || parsed.hostname !== 'localhost') return ''; + if (!parsed) return false; + if (!['http:', 'https:'].includes(parsed.protocol)) return false; + if (parsed.hostname !== 'localhost') return false; + if (parsed.pathname !== '/auth/callback') return false; - const segments = parsed.pathname.split('/').filter(Boolean); - if (!segments.length) { - return parsed.origin; - } + const code = (parsed.searchParams.get('code') || '').trim(); + const state = (parsed.searchParams.get('state') || '').trim(); + return Boolean(code && state); +} - return `${parsed.origin}/${segments[0]}`; +function buildLocalhostCleanupPrefix(rawUrl) { + if (!isLocalhostOAuthCallbackUrl(rawUrl)) return ''; + + const parsed = parseUrlSafely(rawUrl); + return parsed ? `${parsed.origin}/auth` : ''; } function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) { @@ -261,7 +268,9 @@ function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) { && candidate.origin === reference.origin && candidate.pathname.startsWith('/m/'); case 'vps-panel': - return Boolean(reference) && candidate.origin === reference.origin; + return Boolean(reference) + && candidate.origin === reference.origin + && candidate.pathname === reference.pathname; default: return false; } @@ -1126,6 +1135,9 @@ async function handleStepData(step, payload) { break; case 8: if (payload.localhostUrl) { + if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) { + throw new Error('步骤 8 返回了无效的 localhost OAuth 回调地址。'); + } await setState({ localhostUrl: payload.localhostUrl }); broadcastDataUpdate({ localhostUrl: payload.localhostUrl }); } @@ -2252,7 +2264,7 @@ async function executeStep7(state) { } // ============================================================ -// Step 8: Complete OAuth (auto click + localhost listener) +// Step 8: 完成 OAuth(自动点击 + localhost 回调监听) // ============================================================ let webNavListener = null; @@ -2264,13 +2276,10 @@ async function executeStep8(state) { await addLog('步骤 8:正在监听 localhost 回调地址...'); - // Register webNavigation listener (scoped to this step) + // 只在当前步骤内注册 webNavigation 监听 return new Promise((resolve, reject) => { let resolved = false; - let resolveCaptureWait = null; - const captureWait = new Promise((resolveCapture) => { - resolveCaptureWait = resolveCapture; - }); + let signupTabId = null; const cleanupListener = () => { if (webNavListener) { @@ -2285,31 +2294,29 @@ async function executeStep8(state) { }, 120000); webNavListener = (details) => { - if (details.url.startsWith('http://localhost')) { - console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`); + if (resolved || !signupTabId) return; + if (details.tabId !== signupTabId) return; + if (details.frameId !== 0) return; + if (isLocalhostOAuthCallbackUrl(details.url)) { + console.log(LOG_PREFIX, `已捕获 localhost OAuth 回调:${details.url}`); resolved = true; cleanupListener(); clearTimeout(timeout); - if (resolveCaptureWait) resolveCaptureWait(details.url); - - setState({ localhostUrl: details.url }).then(() => { + completeStepFromBackground(8, { localhostUrl: details.url }).then(() => { addLog(`步骤 8:已捕获 localhost 地址:${details.url}`, 'ok'); - setStepStatus(8, 'completed'); - notifyStepComplete(8, { localhostUrl: details.url }); - broadcastDataUpdate({ localhostUrl: details.url }); resolve(); + }).catch((err) => { + reject(err); }); } }; - chrome.webNavigation.onBeforeNavigate.addListener(webNavListener); - // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex") - // with a "继续" button. We locate the button in-page, then click it through - // the debugger Input API directly. + // 步骤 7 之后,认证页会进入 OAuth 同意页,出现“继续”按钮。 + // 这里先在页面内定位按钮,再通过 debugger 输入事件发起点击。 (async () => { try { - let signupTabId = await getTabId('signup-page'); + signupTabId = await getTabId('signup-page'); if (signupTabId) { await chrome.tabs.update(signupTabId, { active: true }); await addLog('步骤 8:已切回认证页,正在准备调试器点击...'); @@ -2318,6 +2325,8 @@ async function executeStep8(state) { await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...'); } + chrome.webNavigation.onBeforeNavigate.addListener(webNavListener); + const clickResult = await sendToContentScript('signup-page', { type: 'STEP8_FIND_AND_CLICK', source: 'background', @@ -2342,10 +2351,13 @@ async function executeStep8(state) { } // ============================================================ -// Step 9: VPS Verify (via vps-panel.js) +// Step 9: CPA 回调验证(通过 vps-panel.js) // ============================================================ async function executeStep9(state) { + if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { + throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。'); + } if (!state.localhostUrl) { throw new Error('缺少 localhost 回调地址,请先完成步骤 8。'); } diff --git a/content/vps-panel.js b/content/vps-panel.js index 1c7b47c..fb7ae54 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -76,6 +76,27 @@ function getActionText(el) { .trim(); } +function parseUrlSafely(rawUrl) { + if (!rawUrl) return null; + try { + return new URL(rawUrl); + } catch { + return null; + } +} + +function isLocalhostOAuthCallbackUrl(rawUrl) { + const parsed = parseUrlSafely(rawUrl); + if (!parsed) return false; + if (!['http:', 'https:'].includes(parsed.protocol)) return false; + if (parsed.hostname !== 'localhost') return false; + if (parsed.pathname !== '/auth/callback') return false; + + const code = (parsed.searchParams.get('code') || '').trim(); + const state = (parsed.searchParams.get('state') || '').trim(); + return Boolean(code && state); +} + function getStatusBadgeElement() { const selectors = [ '#root > div > div > div > main > div > div > div > div > div:nth-child(1) > div > div.OAuthPage-module__cardContent___1sXLA > div.status-badge', @@ -290,18 +311,24 @@ async function step1_getOAuthLink(payload) { } // ============================================================ -// Step 9: CPA Verify — paste localhost URL and submit +// 步骤 9:CPA 回调验证——填写 localhost 回调地址并提交 // ============================================================ async function step9_vpsVerify(payload) { await ensureOAuthManagementPage(payload?.vpsPassword, 9); - // Get localhostUrl from payload (passed directly by background) or fallback to state + // 优先从 payload 读取 localhostUrl;没有时再回退到全局状态 let localhostUrl = payload?.localhostUrl; + if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) { + throw new Error('步骤 9 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 8。'); + } if (!localhostUrl) { log('步骤 9:payload 中没有 localhostUrl,正在从状态中读取...'); const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); localhostUrl = state.localhostUrl; + if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) { + throw new Error('步骤 9 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 8。'); + } } if (!localhostUrl) { throw new Error('未找到 localhost 回调地址,请先完成步骤 8。');