docs: 设计NapCat WebUI Gateway
This commit is contained in:
parent
98c8568184
commit
cdded5b754
@ -0,0 +1,386 @@
|
||||
# QQBot NapCat WebUI Gateway Design
|
||||
|
||||
## Background
|
||||
|
||||
QQBot already manages NapCat containers, WebUI tokens, login sessions, runtime
|
||||
profile evidence, and split account status inside the API. Admin can observe and
|
||||
drive the curated login flow, but it cannot open the original NapCat WebUI for a
|
||||
specific account without manually exposing container ports and tokens.
|
||||
|
||||
The new requirement is to open the original NapCat WebUI from Admin for one
|
||||
QQBot account, with full WebUI operation capability, while keeping NapCat
|
||||
container addresses, WebUI tokens, credentials, and host topology server-side.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add a QQBot account-row operation that opens the selected account's NapCat
|
||||
WebUI in a second-level Admin route page.
|
||||
- Implement NapCat WebUI access through an independent
|
||||
`kt-napcat-webui-gateway` service, not through direct browser access to
|
||||
NapCat container ports.
|
||||
- Bind gateway access sessions to the Admin route page lifecycle.
|
||||
- Allow full NapCat WebUI operations inside the embedded page.
|
||||
- Keep NapCat WebUI tokens, NapCat credentials, container IPs, host ports, and
|
||||
NAS topology out of browser-visible payloads.
|
||||
- Record auditable evidence for session creation, activation, heartbeat,
|
||||
revocation, expiration, and proxy failures.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not reimplement the NapCat WebUI as native Admin components.
|
||||
- Do not make the first version read-only.
|
||||
- Do not expose NapCat container ports to the public network.
|
||||
- Do not let the browser call arbitrary upstream URLs through the gateway.
|
||||
- Do not merge this gateway into the existing login SSE state machine.
|
||||
|
||||
## User Experience
|
||||
|
||||
Admin adds a new WebUI action on the QQBot account list connection/action area.
|
||||
Clicking it navigates to:
|
||||
|
||||
```text
|
||||
/qqbot/account/:accountId/napcat-webui
|
||||
```
|
||||
|
||||
The route page is a remote console, not a drawer. It has:
|
||||
|
||||
- a compact Admin-owned header with back navigation, account identity, container
|
||||
status, session status, refresh session, and close session actions;
|
||||
- one full-height iframe that loads the gateway URL;
|
||||
- a failure state for unauthorized, missing container, WebUI offline, session
|
||||
expired, or gateway unavailable;
|
||||
- no page-level body scrolling beyond the existing Vben layout content area.
|
||||
|
||||
The route page owns the gateway session. Opening the same WebUI from the account
|
||||
list always creates a new route-bound session. Page refresh creates a new
|
||||
session and lets the previous session expire or be revoked by heartbeat timeout.
|
||||
|
||||
## High-level Architecture
|
||||
|
||||
```text
|
||||
Admin account list
|
||||
-> Admin route /qqbot/account/:accountId/napcat-webui
|
||||
-> API: create route-bound gateway session
|
||||
-> Gateway: store active session and NapCat target metadata
|
||||
-> Admin iframe: gateway bootstrap URL
|
||||
-> Gateway: proxy NapCat WebUI HTML, assets, APIs, and WebSocket traffic
|
||||
-> NapCat WebUI container
|
||||
```
|
||||
|
||||
The API remains the authority for Admin authentication and QQBot account
|
||||
binding. The Gateway remains the authority for active WebUI proxy sessions and
|
||||
NapCat WebUI credential exchange.
|
||||
|
||||
## Service Boundaries
|
||||
|
||||
### API service
|
||||
|
||||
The API service owns Admin-facing authorization and account resolution:
|
||||
|
||||
- verify the Admin JWT and QQBot account permissions;
|
||||
- resolve account, selfId, primary NapCat binding, container id/name, WebUI
|
||||
base URL, and WebUI token from existing persistence;
|
||||
- reject missing, offline, or non-owned NapCat targets before a gateway session
|
||||
is created;
|
||||
- call the Gateway internal API with a service-to-service secret;
|
||||
- return only an opaque `sessionId`, `iframeUrl`, and safe display metadata to
|
||||
Admin;
|
||||
- proxy heartbeat/revoke calls from Admin to the Gateway internal API;
|
||||
- write or forward audit events.
|
||||
|
||||
The API must not return `webuiToken`, NapCat credential, upstream host, upstream
|
||||
port, Docker container IP, or NAS SSH details.
|
||||
|
||||
### Gateway service
|
||||
|
||||
`kt-napcat-webui-gateway` is deployed as a separate process, image, and K8s
|
||||
Deployment. It owns:
|
||||
|
||||
- active session storage and TTL enforcement;
|
||||
- one-time iframe bootstrap tickets;
|
||||
- NapCat WebUI credential login using the existing `sha256(token + ".napcat")`
|
||||
contract;
|
||||
- proxying HTTP methods, static assets, WebUI API calls, redirects, cookies, and
|
||||
WebSocket upgrade traffic;
|
||||
- session heartbeat, revocation, expiration, and cleanup;
|
||||
- gateway-side audit events and structured logs.
|
||||
|
||||
The Gateway only proxies to the target already stored in the session. It never
|
||||
accepts a browser-supplied upstream URL.
|
||||
|
||||
### Admin frontend
|
||||
|
||||
Admin owns the route UI and lifecycle:
|
||||
|
||||
- navigate from account list action to the second-level route page;
|
||||
- create a session when the page mounts;
|
||||
- load the returned iframe URL only after session creation succeeds;
|
||||
- send heartbeat while the route is mounted;
|
||||
- revoke the session on route leave, explicit close, component unmount, and
|
||||
best-effort browser unload;
|
||||
- render expired/revoked/error states without blank iframes.
|
||||
|
||||
## Gateway Session Lifecycle
|
||||
|
||||
Sessions are route-bound leases:
|
||||
|
||||
```text
|
||||
created -> active -> revoked
|
||||
created -> expired
|
||||
active -> expired
|
||||
created/active -> failed
|
||||
```
|
||||
|
||||
Creation flow:
|
||||
|
||||
1. Admin route mounts and calls `POST /qqbot/napcat/webui/session`.
|
||||
2. API authorizes the current Admin user and resolves the selected account's
|
||||
active NapCat target.
|
||||
3. API calls Gateway internal `POST /internal/sessions` with safe account
|
||||
metadata plus encrypted or service-internal target credentials.
|
||||
4. Gateway creates an active-session record with TTL and a one-time bootstrap
|
||||
ticket.
|
||||
5. API returns the public iframe bootstrap URL.
|
||||
6. The iframe loads the bootstrap URL. Gateway redeems the one-time ticket,
|
||||
sets an HttpOnly path-scoped gateway cookie, and redirects to the session
|
||||
WebUI root.
|
||||
7. Gateway marks the session active after the first successful proxied WebUI
|
||||
response.
|
||||
|
||||
Heartbeat and cleanup:
|
||||
|
||||
- Admin sends heartbeat every 15 to 30 seconds while the route is mounted.
|
||||
- Gateway updates `lastSeenAt`.
|
||||
- If no heartbeat arrives for 60 to 90 seconds, Gateway expires the session.
|
||||
- Route leave, explicit close, and component unmount call revoke.
|
||||
- `beforeunload` uses a best-effort sendBeacon revoke, but correctness relies on
|
||||
TTL timeout because browser unload is not guaranteed.
|
||||
|
||||
Concurrent access policy:
|
||||
|
||||
- One active WebUI session is allowed per Admin user plus account.
|
||||
- Creating a new session for the same Admin user and account revokes the older
|
||||
session.
|
||||
- Other Admin users require their own authorization and separate sessions.
|
||||
|
||||
Expired or revoked sessions return HTTP 410 for iframe/proxy requests and a
|
||||
small gateway error page that tells the Admin page to reopen the route session.
|
||||
|
||||
## Public and Internal Contracts
|
||||
|
||||
### API Admin contracts
|
||||
|
||||
```text
|
||||
POST /qqbot/napcat/webui/session
|
||||
Body: { accountId: string }
|
||||
Result: {
|
||||
sessionId: string;
|
||||
iframeUrl: string;
|
||||
expiresAt: number;
|
||||
account: { id: string; selfId: string; nickname?: string };
|
||||
container: { id: string; name: string; webuiStatus: string };
|
||||
}
|
||||
|
||||
POST /qqbot/napcat/webui/session/:sessionId/heartbeat
|
||||
Result: { sessionId: string; expiresAt: number; status: "active" }
|
||||
|
||||
POST /qqbot/napcat/webui/session/:sessionId/revoke
|
||||
Result: true
|
||||
```
|
||||
|
||||
The Admin API responses contain only display-safe metadata.
|
||||
|
||||
### Gateway public contracts
|
||||
|
||||
```text
|
||||
GET /napcat-webui/session/:sessionId/bootstrap?ticket=...
|
||||
GET /napcat-webui/session/:sessionId/webui/*
|
||||
POST /napcat-webui/session/:sessionId/webui/*
|
||||
WS /napcat-webui/session/:sessionId/webui/*
|
||||
```
|
||||
|
||||
The bootstrap ticket is short-lived and one-time. After redemption, the browser
|
||||
uses only a path-scoped gateway session cookie. The iframe URL should not keep a
|
||||
long-lived bearer token in the address bar.
|
||||
|
||||
### Gateway internal contracts
|
||||
|
||||
```text
|
||||
POST /internal/sessions
|
||||
POST /internal/sessions/:sessionId/heartbeat
|
||||
POST /internal/sessions/:sessionId/revoke
|
||||
GET /internal/health
|
||||
```
|
||||
|
||||
Internal endpoints require a service-to-service secret or mTLS-equivalent
|
||||
cluster boundary. They are not exposed through the public Caddy/Admin route.
|
||||
|
||||
## Proxy Behavior
|
||||
|
||||
The Gateway must support:
|
||||
|
||||
- all HTTP methods used by the NapCat WebUI;
|
||||
- JSON and binary payloads;
|
||||
- static assets;
|
||||
- redirects with `Location` rewritten back under the gateway session prefix;
|
||||
- cookies rewritten to a session-scoped path and never exposed across accounts;
|
||||
- WebSocket upgrade traffic if NapCat WebUI uses it;
|
||||
- `Cache-Control: no-store` for sensitive proxied responses;
|
||||
- removal or replacement of upstream frame headers that would block Admin's
|
||||
iframe, while preserving safe CSP around the gateway route.
|
||||
|
||||
If NapCat WebUI emits absolute `/api` or `/webui` paths, Gateway rewrites the
|
||||
HTML and response headers so browser requests remain under:
|
||||
|
||||
```text
|
||||
/napcat-webui/session/:sessionId/webui/
|
||||
```
|
||||
|
||||
The proxy must reject path traversal, encoded upstream URL injection, and any
|
||||
request that escapes the bound session prefix.
|
||||
|
||||
## Security Model
|
||||
|
||||
Full NapCat WebUI operations are allowed. Authorization is therefore explicit:
|
||||
an Admin user who can open the route for an account can fully operate that
|
||||
account's NapCat WebUI.
|
||||
|
||||
Security controls:
|
||||
|
||||
- Admin JWT is checked by API before session creation.
|
||||
- Gateway session is opaque, short-lived, and bound to Admin user id, account id,
|
||||
selfId, container id, client metadata, and route lease.
|
||||
- NapCat WebUI token and credential never leave server-side services.
|
||||
- Browser-visible URLs contain at most a one-time bootstrap ticket.
|
||||
- Gateway cookies are HttpOnly, Secure, SameSite, and path-scoped to the
|
||||
session.
|
||||
- Public gateway requests cannot select upstream hosts or ports.
|
||||
- Internal API-to-Gateway calls are authenticated.
|
||||
- Audit records include user id, account id, selfId, container id, session id,
|
||||
event type, client IP, user agent, timestamps, and safe error summaries.
|
||||
|
||||
## Data Model
|
||||
|
||||
Active runtime session state lives in Redis so Gateway replicas can share it:
|
||||
|
||||
```text
|
||||
napcat:webui:session:{sessionId}
|
||||
status
|
||||
adminUserId
|
||||
accountId
|
||||
selfId
|
||||
containerId
|
||||
containerName
|
||||
upstreamBaseUrl
|
||||
encryptedWebuiToken or encrypted credential material
|
||||
createdAt
|
||||
activeAt
|
||||
lastSeenAt
|
||||
expiresAt
|
||||
revokedAt
|
||||
```
|
||||
|
||||
Gateway audit history is persisted in MySQL, either through the API service or a
|
||||
Gateway-owned table managed from the API repository:
|
||||
|
||||
```text
|
||||
qqbot_napcat_webui_gateway_audit
|
||||
id
|
||||
session_id
|
||||
admin_user_id
|
||||
account_id
|
||||
self_id
|
||||
container_id
|
||||
event_type
|
||||
client_ip
|
||||
user_agent
|
||||
detail_json
|
||||
create_time
|
||||
```
|
||||
|
||||
No WebUI token, NapCat credential, QQ password, captcha ticket, QR payload, or
|
||||
raw proxied response is stored in audit rows.
|
||||
|
||||
## Deployment
|
||||
|
||||
The first implementation keeps the service source in the API repository but
|
||||
builds and deploys it as a separate service:
|
||||
|
||||
- independent app entry for `kt-napcat-webui-gateway`;
|
||||
- independent Dockerfile or Docker target;
|
||||
- Jenkins build and push for the Gateway image;
|
||||
- K8s Deployment and Service;
|
||||
- Caddy/Admin domain route such as:
|
||||
|
||||
```text
|
||||
https://admin.kwitsukasa.top/napcat-webui/*
|
||||
```
|
||||
|
||||
The route points to the Gateway service, while the existing API remains under
|
||||
`/api/*`. Local development should provide an equivalent Vite or Caddy proxy so
|
||||
Admin can test the iframe route without exposing NapCat container ports.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Missing account or no active NapCat binding: Admin route shows a clear empty
|
||||
state with a link back to the account list.
|
||||
- Container offline or WebUI unavailable: session creation fails before iframe
|
||||
load and shows the runtime evidence returned by API.
|
||||
- Gateway cannot authenticate to NapCat WebUI: session fails with a sanitized
|
||||
error and an audit event.
|
||||
- Session expired or revoked: iframe receives HTTP 410 and Admin page offers a
|
||||
one-click session reopen.
|
||||
- Gateway target proxy timeout: render an error overlay and keep route controls
|
||||
available.
|
||||
- API and Gateway clocks must not be used as the only correctness boundary;
|
||||
Redis TTL and heartbeat timestamps drive cleanup.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Backend/API:
|
||||
|
||||
- unit test account authorization and session creation DTOs;
|
||||
- unit test no secret fields are returned to Admin;
|
||||
- integration test API creates and revokes Gateway sessions through a mocked
|
||||
internal Gateway client;
|
||||
- audit tests for create, active, heartbeat, revoke, expire, and proxy failure.
|
||||
|
||||
Gateway:
|
||||
|
||||
- unit test bootstrap ticket redemption and one-time use;
|
||||
- unit test session TTL, heartbeat, revoke, and concurrent session revocation;
|
||||
- proxy tests for path rewriting, cookie scoping, redirect rewriting, and
|
||||
forbidden upstream URL injection;
|
||||
- WebSocket upgrade smoke if NapCat WebUI requires it.
|
||||
|
||||
Admin:
|
||||
|
||||
- route test for account-row WebUI action navigation;
|
||||
- lifecycle test for mount create, heartbeat, unmount revoke;
|
||||
- dark-theme and layout tests for the remote console page;
|
||||
- Playwright smoke that opens the route, waits for iframe shell load, leaves the
|
||||
route, and verifies revoke was called.
|
||||
|
||||
Online:
|
||||
|
||||
- deploy API and Gateway through Jenkins/K8s;
|
||||
- verify Gateway health and route availability under the Admin domain;
|
||||
- open the WebUI page for account `1914728559`;
|
||||
- confirm the iframe loads the original NapCat WebUI without exposing tokens or
|
||||
container ports in the browser URL;
|
||||
- perform one safe WebUI operation such as reading login status or opening a
|
||||
settings page;
|
||||
- leave the route and verify the session is revoked or expires by heartbeat
|
||||
timeout.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- Admin has a working second-level NapCat WebUI route per QQBot account.
|
||||
- The Gateway is independently deployed and observable.
|
||||
- Full NapCat WebUI operation works through the iframe route.
|
||||
- Browser-visible data contains no NapCat token, credential, container IP, host
|
||||
port, NAS SSH route, or QQ password.
|
||||
- Session lifecycle follows the Admin route lifecycle and cleans up on route
|
||||
leave or heartbeat timeout.
|
||||
- API, Gateway, Admin tests and online smoke evidence are recorded before the
|
||||
feature is considered complete.
|
||||
@ -0,0 +1,321 @@
|
||||
# QQBot NapCat WebUI Gateway 设计
|
||||
|
||||
## 背景
|
||||
|
||||
QQBot 当前已经由 API 管理 NapCat 容器、WebUI token、登录 session、运行态证据和账号拆分状态。Admin 可以使用定制过的登录弹窗完成扫码、验证码和新设备流程,但还不能从某个 QQBot 账号定点打开原版 NapCat WebUI。
|
||||
|
||||
本次目标是在 Admin 中打开指定账号对应的原版 NapCat WebUI,并允许完整操作,同时不把 NapCat 容器端口、WebUI token、Credential 或 NAS 拓扑暴露给浏览器。
|
||||
|
||||
## 目标
|
||||
|
||||
- 在 QQBot 账号列表的账号连接列或操作区增加 WebUI 入口。
|
||||
- 点击入口后进入二级路由页面,而不是抽屉。
|
||||
- 使用独立 `kt-napcat-webui-gateway` 微服务承载 NapCat WebUI 代理。
|
||||
- Gateway session 跟随二级页面生命周期创建、心跳和销毁。
|
||||
- iframe 内允许 NapCat WebUI 完整操作。
|
||||
- 浏览器侧不暴露 NapCat token、Credential、容器 IP、宿主端口或 NAS 内网信息。
|
||||
- 记录 session 创建、激活、心跳、撤销、过期和代理失败审计证据。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不把 NapCat WebUI 重写成 Admin 原生组件。
|
||||
- 第一版不做只读限制。
|
||||
- 不直接公开 NapCat 容器端口。
|
||||
- 不允许浏览器通过 Gateway 任意代理外部 URL。
|
||||
- 不把该 Gateway 合并到现有 NapCat 登录 SSE 状态机。
|
||||
|
||||
## 页面形态
|
||||
|
||||
Admin 新增二级路由:
|
||||
|
||||
```text
|
||||
/qqbot/account/:accountId/napcat-webui
|
||||
```
|
||||
|
||||
QQBot 账号列表的 WebUI 按钮跳转到该路由。页面是远程控制台,而不是抽屉。页面结构:
|
||||
|
||||
- 顶部是 Admin 自己的轻量控制栏:返回账号列表、账号信息、容器状态、session 状态、刷新 session、关闭 session。
|
||||
- 主体是全高 iframe,加载 Gateway 返回的 NapCat WebUI URL。
|
||||
- 未授权、无容器、WebUI 离线、session 过期、Gateway 不可用时显示明确错误态。
|
||||
- 页面高度遵循 Vben Layout 内容区,不额外制造 body 滚动。
|
||||
|
||||
刷新页面会创建新 session;旧 session 通过主动 revoke 或 heartbeat 超时清理。重复从账号列表打开同一账号时创建新 session,不复用旧 session。
|
||||
|
||||
## 总体架构
|
||||
|
||||
```text
|
||||
Admin 账号列表
|
||||
-> Admin 二级路由 /qqbot/account/:accountId/napcat-webui
|
||||
-> API 创建 route-bound Gateway session
|
||||
-> Gateway 保存活跃 session 与 NapCat 目标信息
|
||||
-> Admin iframe 打开 Gateway bootstrap URL
|
||||
-> Gateway 反代 NapCat WebUI HTML、静态资源、API 和 WebSocket
|
||||
-> NapCat WebUI 容器
|
||||
```
|
||||
|
||||
API 是 Admin 鉴权和 QQBot 账号绑定的权威。Gateway 是 WebUI 代理 session 和 NapCat WebUI Credential 交换的权威。
|
||||
|
||||
## 服务边界
|
||||
|
||||
### API 服务
|
||||
|
||||
API 负责 Admin 侧鉴权和账号解析:
|
||||
|
||||
- 校验 Admin JWT 与 QQBot 账号权限。
|
||||
- 根据账号解析 selfId、主 NapCat 绑定、容器 ID/名称、WebUI base URL 和 WebUI token。
|
||||
- 在创建 Gateway session 前拒绝无容器、容器离线、WebUI 不可用或无权限的请求。
|
||||
- 使用服务间密钥调用 Gateway 内部接口。
|
||||
- 只把 `sessionId`、`iframeUrl` 和安全展示字段返回给 Admin。
|
||||
- 代理 Admin 发来的 heartbeat/revoke 到 Gateway 内部接口。
|
||||
- 写入或转发审计事件。
|
||||
|
||||
API 不返回 `webuiToken`、NapCat Credential、容器 IP、宿主端口、Docker 网络或 NAS SSH 信息。
|
||||
|
||||
### Gateway 服务
|
||||
|
||||
`kt-napcat-webui-gateway` 独立进程、独立镜像、独立 K8s Deployment。它负责:
|
||||
|
||||
- 活跃 session 存储与 TTL 管理。
|
||||
- 一次性 iframe bootstrap ticket。
|
||||
- 按 NapCat 现有契约使用 `sha256(token + ".napcat")` 登录 WebUI 并换取 Credential。
|
||||
- 反代 HTTP 方法、静态资源、WebUI API、重定向、Cookie 和 WebSocket upgrade。
|
||||
- session heartbeat、revoke、expire 和清理。
|
||||
- Gateway 侧审计和结构化日志。
|
||||
|
||||
Gateway 只代理 session 内已绑定的目标容器,不接受浏览器传入的 upstream URL。
|
||||
|
||||
### Admin 前端
|
||||
|
||||
Admin 负责路由页面和生命周期:
|
||||
|
||||
- 账号列表 WebUI 按钮跳转到二级路由。
|
||||
- 页面 mounted 时创建 session。
|
||||
- session 创建成功后再加载 iframe。
|
||||
- 页面 mounted 期间持续 heartbeat。
|
||||
- 路由离开、显式关闭、组件 unmount、浏览器卸载时尽量 revoke。
|
||||
- session 过期或失败时展示可读错误态,避免 iframe 白屏。
|
||||
|
||||
## Gateway Session 生命周期
|
||||
|
||||
session 是页面生命周期租约:
|
||||
|
||||
```text
|
||||
created -> active -> revoked
|
||||
created -> expired
|
||||
active -> expired
|
||||
created/active -> failed
|
||||
```
|
||||
|
||||
创建流程:
|
||||
|
||||
1. Admin 二级页面 mounted,调用 `POST /qqbot/napcat/webui/session`。
|
||||
2. API 校验当前 Admin 用户并解析目标账号的 NapCat 容器。
|
||||
3. API 调用 Gateway 内部 `POST /internal/sessions`,传入安全账号元数据和服务端目标凭据。
|
||||
4. Gateway 创建带 TTL 的 session,并生成一次性 bootstrap ticket。
|
||||
5. API 返回公开 iframe bootstrap URL。
|
||||
6. iframe 打开 bootstrap URL,Gateway 兑换一次性 ticket,设置 HttpOnly、路径隔离的 gateway cookie,并跳转到 session WebUI 根路径。
|
||||
7. Gateway 在第一次成功代理 WebUI 响应后将 session 标记为 active。
|
||||
|
||||
心跳和清理:
|
||||
|
||||
- Admin 页面 mounted 时每 15 到 30 秒发送 heartbeat。
|
||||
- Gateway 更新 `lastSeenAt`。
|
||||
- 60 到 90 秒无 heartbeat 时 Gateway 自动 expire。
|
||||
- 路由离开、显式关闭、组件 unmount 时主动 revoke。
|
||||
- `beforeunload` 使用 sendBeacon 尽量 revoke;正确性仍依赖 TTL,因为浏览器卸载不保证执行。
|
||||
|
||||
并发策略:
|
||||
|
||||
- 同一个 Admin 用户对同一个 QQBot 账号只允许一个活跃 WebUI session。
|
||||
- 创建新 session 时撤销旧 session。
|
||||
- 不同 Admin 用户需要各自鉴权并创建独立 session。
|
||||
|
||||
revoked 或 expired 后,Gateway 对 iframe/proxy 请求返回 410,并展示“会话已关闭,请重新打开”的轻量错误页。
|
||||
|
||||
## 接口契约
|
||||
|
||||
### API Admin 接口
|
||||
|
||||
```text
|
||||
POST /qqbot/napcat/webui/session
|
||||
Body: { accountId: string }
|
||||
Result: {
|
||||
sessionId: string;
|
||||
iframeUrl: string;
|
||||
expiresAt: number;
|
||||
account: { id: string; selfId: string; nickname?: string };
|
||||
container: { id: string; name: string; webuiStatus: string };
|
||||
}
|
||||
|
||||
POST /qqbot/napcat/webui/session/:sessionId/heartbeat
|
||||
Result: { sessionId: string; expiresAt: number; status: "active" }
|
||||
|
||||
POST /qqbot/napcat/webui/session/:sessionId/revoke
|
||||
Result: true
|
||||
```
|
||||
|
||||
这些接口只返回展示安全字段。
|
||||
|
||||
### Gateway 公开接口
|
||||
|
||||
```text
|
||||
GET /napcat-webui/session/:sessionId/bootstrap?ticket=...
|
||||
GET /napcat-webui/session/:sessionId/webui/*
|
||||
POST /napcat-webui/session/:sessionId/webui/*
|
||||
WS /napcat-webui/session/:sessionId/webui/*
|
||||
```
|
||||
|
||||
bootstrap ticket 短期有效且只能使用一次。ticket 兑换后,浏览器只依赖路径隔离的 gateway cookie,不在地址栏长期保留 bearer token。
|
||||
|
||||
### Gateway 内部接口
|
||||
|
||||
```text
|
||||
POST /internal/sessions
|
||||
POST /internal/sessions/:sessionId/heartbeat
|
||||
POST /internal/sessions/:sessionId/revoke
|
||||
GET /internal/health
|
||||
```
|
||||
|
||||
内部接口需要服务间密钥或等价的集群内边界,不通过公开 Admin 域暴露。
|
||||
|
||||
## 代理行为
|
||||
|
||||
Gateway 必须支持:
|
||||
|
||||
- NapCat WebUI 使用的全部 HTTP 方法。
|
||||
- JSON 和二进制 payload。
|
||||
- 静态资源。
|
||||
- `Location` 重写到 Gateway session 前缀下。
|
||||
- Cookie path 改写到当前 session,避免跨账号串用。
|
||||
- WebSocket upgrade。
|
||||
- 敏感响应使用 `Cache-Control: no-store`。
|
||||
- 处理或替换会阻止 Admin iframe 的 upstream frame header,同时保留 Gateway 自身安全 CSP。
|
||||
|
||||
如果 NapCat WebUI 输出绝对 `/api` 或 `/webui` 路径,Gateway 需要重写 HTML 和响应头,让浏览器请求始终留在:
|
||||
|
||||
```text
|
||||
/napcat-webui/session/:sessionId/webui/
|
||||
```
|
||||
|
||||
Gateway 必须拒绝路径穿越、编码 URL 注入和任何逃逸当前 session 前缀的请求。
|
||||
|
||||
## 安全模型
|
||||
|
||||
iframe 内允许 NapCat WebUI 完整操作。因此权限语义必须直接明确:能进入该账号 NapCat WebUI 二级页面的 Admin 用户,就拥有该账号 NapCat WebUI 的完整操作权。
|
||||
|
||||
安全控制:
|
||||
|
||||
- API 创建 session 前校验 Admin JWT。
|
||||
- Gateway session 绑定 Admin 用户 ID、账号 ID、selfId、容器 ID、客户端信息和页面租约。
|
||||
- NapCat WebUI token 与 Credential 只存在服务端。
|
||||
- 浏览器可见 URL 最多包含一次性 bootstrap ticket。
|
||||
- Gateway cookie 使用 HttpOnly、Secure、SameSite,并按 session path 隔离。
|
||||
- 公开 Gateway 请求不能选择 upstream host 或 port。
|
||||
- API 到 Gateway 的内部调用必须鉴权。
|
||||
- 审计记录用户、账号、selfId、容器、session、事件类型、IP、UA、时间和安全错误摘要。
|
||||
|
||||
## 数据模型
|
||||
|
||||
活跃 session 使用 Redis 保存,便于 Gateway 多副本共享:
|
||||
|
||||
```text
|
||||
napcat:webui:session:{sessionId}
|
||||
status
|
||||
adminUserId
|
||||
accountId
|
||||
selfId
|
||||
containerId
|
||||
containerName
|
||||
upstreamBaseUrl
|
||||
encryptedWebuiToken or encrypted credential material
|
||||
createdAt
|
||||
activeAt
|
||||
lastSeenAt
|
||||
expiresAt
|
||||
revokedAt
|
||||
```
|
||||
|
||||
审计历史持久化到 MySQL,可由 API 写入,也可以由 Gateway 通过 API 仓库管理的表写入:
|
||||
|
||||
```text
|
||||
qqbot_napcat_webui_gateway_audit
|
||||
id
|
||||
session_id
|
||||
admin_user_id
|
||||
account_id
|
||||
self_id
|
||||
container_id
|
||||
event_type
|
||||
client_ip
|
||||
user_agent
|
||||
detail_json
|
||||
create_time
|
||||
```
|
||||
|
||||
审计表不保存 WebUI token、NapCat Credential、QQ 密码、验证码 ticket、二维码内容或原始代理响应。
|
||||
|
||||
## 部署
|
||||
|
||||
第一版源码放在 API 仓库,但作为独立服务构建和部署:
|
||||
|
||||
- 独立 `kt-napcat-webui-gateway` app entry。
|
||||
- 独立 Dockerfile 或 Docker build target。
|
||||
- Jenkins 构建并推送 Gateway 镜像。
|
||||
- K8s 独立 Deployment 和 Service。
|
||||
- Caddy/Admin 域增加路由,例如:
|
||||
|
||||
```text
|
||||
https://admin.kwitsukasa.top/napcat-webui/*
|
||||
```
|
||||
|
||||
该路由指向 Gateway 服务,现有 API 继续位于 `/api/*`。本地开发需要等价 Vite 或 Caddy proxy,避免为了 iframe 调试而暴露 NapCat 容器端口。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 账号不存在或无有效 NapCat 绑定:页面显示空状态并提供返回账号列表入口。
|
||||
- 容器离线或 WebUI 不可用:session 创建前失败,并展示 API 返回的运行态证据。
|
||||
- Gateway 无法登录 NapCat WebUI:session failed,展示脱敏错误并写审计。
|
||||
- session 过期或撤销:iframe 收到 410,Admin 页面提供重新打开按钮。
|
||||
- Gateway 代理目标超时:显示错误覆盖层,保留页面顶部控制栏。
|
||||
- API/Gateway 不只依赖本机时钟判断正确性,清理以 Redis TTL 和 heartbeat 时间戳为准。
|
||||
|
||||
## 验证策略
|
||||
|
||||
API:
|
||||
|
||||
- 单测账号权限和 session 创建 DTO。
|
||||
- 单测 Admin 响应不包含敏感字段。
|
||||
- 集成测试 API 通过 mock Gateway client 创建、心跳和撤销 session。
|
||||
- 审计测试覆盖 create、active、heartbeat、revoke、expire、proxy failure。
|
||||
|
||||
Gateway:
|
||||
|
||||
- 单测 bootstrap ticket 兑换和一次性使用。
|
||||
- 单测 session TTL、heartbeat、revoke 和同用户同账号并发 session 撤销。
|
||||
- 代理测试覆盖路径重写、Cookie 隔离、重定向重写、禁止 upstream URL 注入。
|
||||
- 如果 NapCat WebUI 使用 WebSocket,则增加 WebSocket upgrade smoke。
|
||||
|
||||
Admin:
|
||||
|
||||
- 账号行 WebUI 按钮路由跳转测试。
|
||||
- 页面生命周期测试:mounted 创建 session、heartbeat、unmount revoke。
|
||||
- 暗色主题和 Layout 高度测试。
|
||||
- Playwright smoke:打开二级路由、等待 iframe shell 加载、离开路由、确认 revoke 被调用。
|
||||
|
||||
线上:
|
||||
|
||||
- Jenkins/K8s 部署 API 和 Gateway。
|
||||
- 验证 Gateway health 与 Admin 域 `/napcat-webui/*` 路由。
|
||||
- 打开账号 `1914728559` 的 WebUI 页面。
|
||||
- 确认 iframe 加载原版 NapCat WebUI,浏览器 URL 不暴露 token、Credential、容器端口。
|
||||
- 执行一个安全 WebUI 操作,例如读取登录状态或打开设置页。
|
||||
- 离开路由后确认 session revoke 或 heartbeat 超时回收。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- Admin 支持按 QQBot 账号打开二级 NapCat WebUI 页面。
|
||||
- Gateway 独立部署且可观测。
|
||||
- iframe 中完整 NapCat WebUI 操作可用。
|
||||
- 浏览器侧不暴露 NapCat token、Credential、容器 IP、宿主端口、NAS SSH 路由或 QQ 密码。
|
||||
- session 生命周期跟随 Admin 二级页面,路由离开或 heartbeat 超时后会清理。
|
||||
- API、Gateway、Admin 测试和线上 smoke 证据齐全后才算完成。
|
||||
Loading…
Reference in New Issue
Block a user