diff --git a/backend/app/services/auth_capture_service.py b/backend/app/services/auth_capture_service.py index fe4fdae..c861d68 100644 --- a/backend/app/services/auth_capture_service.py +++ b/backend/app/services/auth_capture_service.py @@ -59,51 +59,17 @@ async def extract_session_storage(page: Any) -> dict[str, str]: return {} -async def extract_request_headers(page: Any) -> list[dict[str, str]]: - """Capture Authorization headers from network requests via CDP. +async def extract_request_headers(session: Any) -> list[dict[str, str]]: + """Return Authorization / API-Key headers captured continuously by CDP. - Uses Chrome DevTools Protocol to subscribe to Network.requestWillBeSent - events and extract Authorization / X-API-Key headers from captured - requests. Only catches requests made *after* CDP is enabled. + The CDP Network listener is started when the ephemeral session is created + (in BrowserSessionService.create_ephemeral), so headers from the login + flow are captured in real-time without needing a fresh CDP attach. """ - captured: list[dict[str, str]] = [] - cdp = None - try: - cdp = await page.context.new_cdp_session(page) - await cdp.send("Network.enable") - - def on_request(params: dict) -> None: - headers = params.get("request", {}).get("headers", {}) - auth = (headers.get("authorization") or headers.get("Authorization")) - api_key = (headers.get("x-api-key") or headers.get("X-API-Key")) - if auth: - captured.append({ - "type": "authorization", - "value": auth, - "url": params.get("request", {}).get("url", ""), - }) - logger.debug("auth-capture CDP: captured Authorization header") - if api_key: - captured.append({ - "type": "api_key", - "value": api_key, - "url": params.get("request", {}).get("url", ""), - }) - logger.debug("auth-capture CDP: captured X-API-Key header") - - cdp.on("Network.requestWillBeSent", on_request) - # Give a moment for any in-flight requests to be captured - import asyncio - await asyncio.sleep(0.5) - except Exception as exc: - logger.debug("CDP network capture not available: %s", exc) - finally: - if cdp: - try: - await cdp.detach() - except Exception: - pass - return captured + if hasattr(session, "captured_headers") and session.captured_headers: + logger.debug("auth-capture: returning %d cached headers", len(session.captured_headers)) + return list(session.captured_headers) + return [] async def extract_all(session: Any) -> dict[str, Any]: @@ -116,7 +82,7 @@ async def extract_all(session: Any) -> dict[str, Any]: cookies = await extract_cookies(session) local_storage = await extract_local_storage(page) session_storage = await extract_session_storage(page) - auth_headers = await extract_request_headers(page) + auth_headers = await extract_request_headers(session) candidates = _curate_candidates(cookies, local_storage, session_storage, auth_headers) return { @@ -137,22 +103,21 @@ def _curate_candidates( """Scan extracted data for likely credentials with confidence scoring.""" candidates: list[dict[str, Any]] = [] - # 1. CDP-captured Authorization headers (highest confidence) + # 1. CDP-captured network headers (highest confidence) seen = set() for h in auth_headers: dedup_key = h["value"] if dedup_key in seen: continue seen.add(dedup_key) + htype = h.get("type", "authorization") preview = _preview(h["value"]) - candidates.append({ - "type": "bearer_token", - "source": f"network:{h['url'][:60]}", - "value": h["value"], - "preview": preview, - "label": f"Authorization — {h['url'][:40]}", - "confidence": 95, - }) + if htype == "api_key": + _add(candidates, "api_key", f"network:{h['url'][:60]}", h["value"], preview, + f"X-API-Key — {h['url'][:40]}", 95) + else: + _add(candidates, "bearer_token", f"network:{h['url'][:60]}", h["value"], preview, + f"Authorization — {h['url'][:40]}", 95) # 2. localStorage/sessionStorage items for store_name, store in [("localStorage", local_storage), ("sessionStorage", session_storage)]: diff --git a/backend/app/services/browser_session_service.py b/backend/app/services/browser_session_service.py index d03638e..6c06d26 100644 --- a/backend/app/services/browser_session_service.py +++ b/backend/app/services/browser_session_service.py @@ -32,6 +32,8 @@ class BrowserSession: context: Any page: Any lock: asyncio.Lock + cdp_session: Any = None + captured_headers: list[dict] = None # auth headers from CDP class BrowserSessionService: @@ -163,10 +165,24 @@ class BrowserSessionService: session = self._discard_session(session_id) if not session: return + # Detach CDP session if active + if session.cdp_session: + try: + await session.cdp_session.detach() + except Exception: + pass try: await session.context.close() except Exception: pass + # Clean up ephemeral (auth-capture) profile directories + if session.profile_key and session.profile_key.startswith("auth-capture-"): + profile_dir = self._profile_dir(session.profile_key) + import shutil + try: + shutil.rmtree(profile_dir, ignore_errors=True) + except Exception: + pass async def shutdown(self) -> None: sessions = list(self._sessions) @@ -368,14 +384,46 @@ class BrowserSessionService: context=context, page=page, lock=asyncio.Lock(), + captured_headers=[], ) self._sessions[session.id] = session try: await page.goto(url, wait_until="domcontentloaded", timeout=45000) + # Start CDP network capture immediately — so we don't miss login requests + await self._start_cdp_capture(session) except Exception: await self.close(session.id) raise return session + async def _start_cdp_capture(self, session: BrowserSession) -> None: + """Enable CDP Network domain and capture Authorization headers.""" + try: + cdp = await session.context.new_cdp_session(session.page) + await cdp.send("Network.enable") + + def on_request(params: dict) -> None: + headers = params.get("request", {}).get("headers", {}) + auth = headers.get("authorization") or headers.get("Authorization") + api_key = headers.get("x-api-key") or headers.get("X-API-Key") + url = params.get("request", {}).get("url", "") + if auth: + session.captured_headers.append({ + "type": "authorization", + "value": auth, + "url": url, + }) + if api_key: + session.captured_headers.append({ + "type": "api_key", + "value": api_key, + "url": url, + }) + + cdp.on("Network.requestWillBeSent", on_request) + session.cdp_session = cdp + except Exception as exc: + logger.debug("CDP capture not available: %s", exc) + browser_sessions = BrowserSessionService() diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 4c6df16..3d6236d 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -339,7 +339,7 @@ export interface AuthCaptureResult { session_storage: Record auth_headers: Record[] candidates: { - type: 'bearer_token' | 'cookie' | 'credential' + type: 'bearer_token' | 'cookie' | 'credential' | 'api_key' source: string value: string preview: string diff --git a/frontend/src/components/AuthCaptureDialog.vue b/frontend/src/components/AuthCaptureDialog.vue index 77d26a2..59c1cb3 100644 --- a/frontend/src/components/AuthCaptureDialog.vue +++ b/frontend/src/components/AuthCaptureDialog.vue @@ -15,22 +15,13 @@ -
+
- - - - - - - - -
@@ -44,18 +35,18 @@
- +

步骤 2:在浏览器中手动登录

- + - + - + @@ -64,39 +55,43 @@ 提取认证信息
+
+ + {{ wsConnected ? '实时' : '连接中…' }} +

在下方浏览器中完成登录后,点击「提取认证信息」获取 token / cookie。

- +
远程浏览器
-

正在启动浏览器…

+

正在连接远程浏览器…

- +
@@ -117,16 +112,16 @@
- {{ c.type === 'bearer_token' ? 'Bearer' : c.type === 'cookie' ? 'Cookie' : 'Key' }} + {{ badgeLabel(c.type) }} {{ c.label }} - + {{ c.confidence }}%
- {{ maskValue(c.preview || c.value) }} + {{ c.preview || maskValue(c.value) }}
@@ -144,7 +139,7 @@ diff --git a/frontend/src/views/Upstreams.vue b/frontend/src/views/Upstreams.vue index a74931c..a45e6a6 100644 --- a/frontend/src/views/Upstreams.vue +++ b/frontend/src/views/Upstreams.vue @@ -460,13 +460,30 @@ function handleAuthCaptureSelect(candidate: { type: string; value: string; cooki if (candidate.type === 'bearer_token') { form.value.auth_type = 'bearer' form.value.auth_config.token = candidate.value + ElMessage.success('已填入 Bearer Token') } else if (candidate.type === 'cookie') { form.value.auth_type = 'cookie' form.value.auth_config.cookie_string = candidate.cookie_name && candidate.cookie_value ? `${candidate.cookie_name}=${candidate.cookie_value}` : candidate.value + ElMessage.success('已填入 Cookie') + } else if (candidate.type === 'api_key') { + form.value.auth_type = 'api_key' + form.value.auth_config.key = candidate.value + form.value.auth_config.header = 'X-API-Key' + ElMessage.success('已填入 API Key') + } else if (candidate.type === 'credential') { + // Try to guess — if value starts with 'sk-', treat as bearer + if (candidate.value.startsWith('sk-')) { + form.value.auth_type = 'bearer' + form.value.auth_config.token = candidate.value + ElMessage.success('已填入 Bearer Token (sk-key)') + } else { + form.value.auth_type = 'bearer' + form.value.auth_config.token = candidate.value + ElMessage.success('已填入认证信息') + } } - ElMessage.success(`已填入${candidate.type === 'cookie' ? 'Cookie' : 'Bearer Token'}认证信息`) } const quickPlatform = ref('sub2api')