174 lines
7.2 KiB
Markdown
174 lines
7.2 KiB
Markdown
# 服务端远程浏览器(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 依赖仅当确认不再使用
|