fix(browser): 门控 — Worker TLS verify 可配置、审计清单与 shell LF

默认校验 Worker HTTPS 证书;审计文件补全变更清单;docker-entrypoint.sh 改为 LF。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-12 06:50:06 +08:00
parent e833b923c8
commit dbb6bc4a55
3 changed files with 39 additions and 18 deletions
+31 -15
View File
@@ -6,21 +6,37 @@
## 范围(文件清单)
| 文件 | 行数 | 职责 |
|------|------|------|
| `browser-worker/main.py` | 147 | Worker HTTP/WS API |
| `browser-worker/session_manager.py` | 323 | Playwright + screencast |
| `browser-worker/browser_url_safe.py` | 79 | SSRFWorker 侧) |
| `server/api/browser_session.py` | 198 | 桥接 REST + `/ws/browser` |
| `server/infrastructure/browser/worker_client.py` | 83 | 连 Worker |
| `server/infrastructure/browser/session_registry.py` | 52 | 内存会话归属 |
| `server/utils/browser_url_safe.py` | 80 | SSRF(桥接层) |
| `server/api/browser.py` | 88 | UI 状态 `/api/browser/state` |
| `server/utils/browser_ui_state.py` | 165 | 状态解析 |
| `frontend/src/composables/useRemoteBrowserSession.ts` | 213 | canvas + WS |
| `frontend/src/components/GlobalBrowserPanel.vue` | 380 | 浮动面板 |
| `tests/test_browser_url_safe.py` | 32 | SSRF 单测 |
| `tests/test_browser_ui_state.py` | 35 | UI 状态单测 |
| 文件 | 职责 |
|------|------|
| `browser-worker/Dockerfile` | Worker 镜像 |
| `browser-worker/docker-compose.yml` | Worker 编排 |
| `browser-worker/docker-entrypoint.sh` | headed + Xvfb 入口 |
| `browser-worker/requirements.txt` | Worker 依赖 |
| `browser-worker/config.py` | Worker 环境配置 |
| `browser-worker/main.py` | Worker HTTP/WS API |
| `browser-worker/session_manager.py` | Playwright + screencast |
| `browser-worker/browser_stealth.py` | 反自动化 / 验证码辅助 |
| `browser-worker/browser_url_safe.py` | SSRFWorker 侧) |
| `server/main.py` | 注册 browser 路由 |
| `server/config.py` | `BROWSER_*` 与 TLS verify |
| `server/api/settings.py` | 设置项默认值(push_complete_sound |
| `server/api/browser_session.py` | 桥接 REST + `/ws/browser` |
| `server/api/browser.py` | UI 状态 `/api/browser/state` |
| `server/infrastructure/browser/__init__.py` | 包入口 |
| `server/infrastructure/browser/worker_client.py` | 连 Worker(可配置 TLS verify |
| `server/infrastructure/browser/session_registry.py` | 内存会话归属 |
| `server/utils/browser_url_safe.py` | SSRF(桥接层) |
| `server/utils/browser_ui_state.py` | 状态解析 |
| `frontend/src/App.vue` | 顶栏 + 固定 GlobalBrowserPanel |
| `frontend/src/router/index.ts` | `/browser` 路由 |
| `frontend/src/pages/ServersPage.vue` | 服务器页打开浏览器 |
| `frontend/src/pages/BrowserPage.vue` | 独立浏览器页(兼容) |
| `frontend/src/components/GlobalBrowserPanel.vue` | 固定顶栏下整窗 |
| `frontend/src/composables/useGlobalBrowser.ts` | 全局标签/状态 |
| `frontend/src/composables/useRemoteBrowserSession.ts` | canvas + WS |
| `tests/test_browser_url_safe.py` | SSRF 单测 |
| `tests/test_browser_ui_state.py` | UI 状态单测 |
| `tests/test_settings_sound_defaults.py` | 设置默认音单测 |
## API / WS 入口表
+1
View File
@@ -114,6 +114,7 @@ class Settings(BaseSettings):
BROWSER_WORKER_SECRET: str = ""
BROWSER_MAX_SESSIONS: int = 2
BROWSER_IDLE_TIMEOUT_SEC: int = 1800
BROWSER_WORKER_TLS_VERIFY: str = "true"
# extra=ignore: .env 中与 MCP 共存的 MYSQL_* / NEXUS_API_BASE_URL 等不进入 Settings
model_config = SettingsConfigDict(
@@ -31,6 +31,10 @@ def _headers() -> dict[str, str]:
return {"X-Nexus-Worker-Key": settings.BROWSER_WORKER_SECRET}
def _tls_verify() -> bool:
return settings.BROWSER_WORKER_TLS_VERIFY.lower() in ("1", "true", "yes")
def worker_stream_url(session_id: str) -> str:
base = settings.BROWSER_WORKER_URL.rstrip("/")
parsed = urlparse(base)
@@ -46,7 +50,7 @@ async def create_worker_session(session_id: str, viewport_w: int, viewport_h: in
url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/v1/sessions"
payload = {"session_id": session_id, "viewport_w": viewport_w, "viewport_h": viewport_h}
try:
async with httpx.AsyncClient(timeout=30.0, verify=False) as client:
async with httpx.AsyncClient(timeout=30.0, verify=_tls_verify()) as client:
resp = await client.post(url, json=payload, headers=_headers())
except httpx.RequestError as exc:
logger.warning("worker create failed: %s", exc)
@@ -62,7 +66,7 @@ async def delete_worker_session(session_id: str) -> None:
return
url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/v1/sessions/{session_id}"
try:
async with httpx.AsyncClient(timeout=15.0, verify=False) as client:
async with httpx.AsyncClient(timeout=15.0, verify=_tls_verify()) as client:
await client.delete(url, headers=_headers())
except httpx.RequestError as exc:
logger.warning("worker delete failed session=%s: %s", session_id, exc)
@@ -73,7 +77,7 @@ async def worker_health() -> dict[str, Any] | None:
return None
url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/health"
try:
async with httpx.AsyncClient(timeout=5.0, verify=False) as client:
async with httpx.AsyncClient(timeout=5.0, verify=_tls_verify()) as client:
resp = await client.get(url)
if resp.status_code == 200:
return resp.json()