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 依赖仅当确认不再使用
|