feat: auth-capture — WS frame stream, drag events, continuous CDP, profile cleanup

- AuthCaptureDialog: real WebSocket for binary JPEG frame stream (no polling)
- Pointer drag: mousedown/mousemove/mouseup events for slider-captcha
- CDP capture starts at session creation, caches headers in session.captured_headers
- Ephemeral profile dir deleted on session close (shutil.rmtree)
- Candidate types unified: bearer_token / cookie / api_key / credential
- Frontend handleAuthCaptureSelect maps all 4 types to correct form fields
This commit is contained in:
SmartUp Developer
2026-05-18 14:14:33 +08:00
parent 08c855677a
commit c7b33983d6
5 changed files with 258 additions and 296 deletions
+18 -53
View File
@@ -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)]:
@@ -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()