# 服务端远程浏览器（Playwright Chromium）— 设计说明

> **取代** iframe 渲染方案（[2026-06-11-embedded-browser-design.md](./2026-06-11-embedded-browser-design.md) 中的页面加载方式）。  
> UI 状态（标签、收藏 `nav_links`、窗口位置）仍走 `/api/browser/state`，不变。

## 背景与目标

运维需要在 Nexus 浮动面板内**完整浏览任意公网站点**，不受 `X-Frame-Options` / CSP `frame-ancestors` 限制，且使用**服务端独立浏览器内核**，而非用户本机标签页内的 iframe。

### 用户确认（2026-06-11）

| 决策项 | 选择 |
|--------|------|
| 可访问范围 | **任意公网** `http` / `https` |
| 内核 | **Chromium**（Playwright），不要求 Firefox |
| 交互 | 在 Nexus 面板内看画面 + 键鼠操作（非「新标签打开」为主） |

### 与 iframe 方案对比

| | iframe（现状） | 服务端 Chromium（目标） |
|--|----------------|---------------------------|
| 渲染位置 | 用户浏览器 | Nexus 服务器 |
| 站点嵌入限制 | 常被拒绝 | 无（顶层窗口） |
| SSRF 风险 | 无 | **有，必须缓解** |
| 资源 | 几乎无 | 每会话 ~200–500MB RAM |
| Cookie / 登录 | 用户本机 | **服务器浏览器 Profile** |

## 方案概述

类比 RDP（guacd → WebSocket → 前端画布），但 Nexus **拥有**浏览器进程与协议：

```
管理员浏览器 (Vue SPA)
    │  JWT WebSocket + REST
    ▼
Nexus API (FastAPI) — 桥接，不跑 Chromium
    │  Worker Key + 内网 HTTPS
    ▼
Browser Worker VPS — Playwright Chromium（headless + CDP screencast）
    ▼
公网目标站点
```

| 组件 | 职责 |
|------|------|
| **Playwright Chromium** | 运行在 **Browser Worker**；Nexus 桥接会话 |
| **URL 安全网关** | 导航前 DNS 解析 + IP 段校验，阻断私网/元数据 SSRF |
| **`/ws/browser/{session_id}`** | 推送 JPEG 帧；接收 navigate / 键鼠 / 滚动 |
| **`POST/DELETE /api/browser/sessions`** | 创建、销毁会话 |
| **`GlobalBrowserPanel`** | `<canvas>` 替代 `<iframe>`；保留地址栏、标签、收藏、最小化 |
| **`/api/browser/state`** | 不变 — tabs、visits、nav_links、rect |

**刻意不做（首版）**

- 不做 HTTP 反向代理（HTML 改写 / Cookie 注入）
- 不做浏览内容审计或页面录制落库
- 不做多管理员并发压测级扩展（单人运维，全局并发 ≤2）
- 不做本机 Cookie 与服务器 Profile 双向同步

## WebSocket 协议（草案）

鉴权：与终端/RDP 一致，query `?token=` = 常规 access JWT（`sub` = admin_id），且 `session` 归属该校验通过的管理员。

### 服务端 → 客户端

| type | 字段 | 说明 |
|------|------|------|
| `BROWSER_INIT` | `session_id`, `viewport_w`, `viewport_h` | 连接就绪 |
| `SCREENCAST_FRAME` | `data_b64`, `metadata` | CDP screencast JPEG |
| `PAGE_INFO` | `url`, `title` | 导航完成或标题变化 |
| `ERROR` | `message`, `code` | URL 拒绝、超时、Playwright 错误 |
| `PONG` | — | 心跳 |

### 客户端 → 服务端

| type | 字段 | 说明 |
|------|------|------|
| `NAVIGATE` | `url` | 经 SSRF 校验后 `page.goto` |
| `RELOAD` / `GO_BACK` / `GO_FORWARD` | — | 历史导航 |
| `MOUSE` | `event`, `x`, `y`, `button` | move / down / up / wheel |
| `KEY` | `event`, `key`, `code`, `text` | down / up / type |
| `VIEWPORT` | `width`, `height` | 面板尺寸变化 |
| `PING` | — | 心跳 |

坐标：`x/y` 为相对 viewport 的 CSS 像素，与 screencast 帧尺寸一致。

## SSRF 防护（任意公网）

导航与每次 redirect 后 **重新校验** 最终 URL：

1. 仅 `http:` / `https:`；拒绝凭据、`javascript:`、`data:`、`file:`
2. 解析 hostname → `getaddrinfo` → **所有**解析 IP 必须不在阻断段：
   - `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`
   - `169.254.0.0/16`, `0.0.0.0/8`, `100.64.0.0/10`
   - IPv6: `::1`, `fc00::/7`, `fe80::/10`
3. 阻断常见 metadata 主机名（如 `169.254.169.254`、`*metadata*` 字面）
4. 导航超时 30s；单页资源不限制由 Chromium 自身处理
5. 失败时 WS 返回 `ERROR`，**不**静默降级

> 单人运维下 residual risk：DNS rebinding 需 TTL 短重查或 redirect 链逐跳校验；首版采用 **redirect 监听 + 每跳校验**。

## 会话与资源

| 策略 | 值 |
|------|-----|
| 每管理员活跃会话 | 1 |
| 全局活跃会话 | ≤2 |
| 空闲超时 | 30 min 无 WS 活动 → 关闭 browser context |
| 视口默认 | 1280×720（可随面板 resize） |
| Screencast | CDP `Page.startScreencast`, JPEG quality ~80 |

进程模型：单例 Playwright `Browser` 进程 + 每会话独立 `BrowserContext`（隔离 Cookie）。

## 部署（选定：独立 Worker 服务器）

> **购机与装机计划**：[`2026-06-11-browser-worker-server-procurement.md`](../plans/2026-06-11-browser-worker-server-procurement.md)

Playwright Chromium **不装入 Nexus 主站镜像**，而在**单独 VPS（Browser Worker）**运行；Nexus 主站仅桥接 WebSocket。

```
管理员浏览器 → Nexus API（JWT）→ Browser Worker（Worker Key）→ 公网
```

| 组件 | 部署位置 |
|------|----------|
| `browser-worker/` 服务 + Chromium | **Worker VPS**（2C4G 起） |
| `/api/browser/sessions`、`/ws/browser/*` 桥接 | **Nexus 主站** |
| MySQL UI 状态 | Nexus 主站（不变） |

**Nexus 主站 `.env`**

```env
NEXUS_BROWSER_ENABLED=1
NEXUS_BROWSER_WORKER_URL=https://<worker-internal-or-vpn>:8443
NEXUS_BROWSER_WORKER_SECRET=<shared-secret>
NEXUS_BROWSER_MAX_SESSIONS=2
NEXUS_BROWSER_IDLE_TIMEOUT_SEC=1800
```

**Worker 防火墙**：8443 **仅** Nexus 主站 IP；不对公网开放 Worker API。

**降级**：`NEXUS_BROWSER_ENABLED=0` 或 Worker 不可达 → `POST /api/browser/sessions` **503**；前端提示「服务端浏览器未启用」。

~~`Dockerfile.prod` 增加 Chromium~~ — **Worker 模式不修改主站镜像体积**。

## 前端变更

- `GlobalBrowserPanel.vue`：`<iframe>` → `<canvas>` + `useRemoteBrowserSession.ts`
- 保留：浮动/最小化/不可关闭/重启、标签、历史、收藏（nav_links 待做 UI）
- 移除或隐藏：「新标签打开」可保留作 **导出 URL** 辅助，非主路径
- `useGlobalBrowser.ts`：`navigate` 改为 WS `NAVIGATE`；`frameKey` 不再需要

## 安全与隐私

- 页面内容经 JPEG 帧过 Nexus，**不持久化**截图
- 服务器 Chromium 内 Cookie 存于容器 ephemeral context（重启会话丢失）
- 仅 `get_current_admin` 可创建/连接会话
- CUD：创建/销毁会话可选写 audit（首版 **不做**，与 RDP 无连接审计一致）

## 验收标准

- [ ] 打开 `https://example.com` 等公网站，面板 canvas 有画面且可点击链接
- [ ] 曾 iframe 白屏的站点（如带 `X-Frame-Options: DENY` 的站）可正常显示
- [ ] 访问 `http://127.0.0.1` / `http://10.x` / metadata → 明确 ERROR，不发起请求
- [ ] 刷新页面后会话内 Cookie 保留；重启浏览器会话 Cookie 清空
- [ ] 标签、收藏、窗口位置仍持久化到 `/api/browser/state`
- [ ] `NEXUS_BROWSER_ENABLED=0` 时有可读降级提示
- [ ] Worker VPS 部署完成，防火墙仅 Nexus 主站可连

## 回滚

- 设 `NEXUS_BROWSER_ENABLED=0` → 503，前端可回退 iframe 分支（保留 feature flag 一段过渡期）
- 卸载 Playwright 依赖仅当确认不再使用
