Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const (

// DefaultCSPPolicy is the default Content-Security-Policy with nonce support
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://accounts.google.com/gsi/client https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://accounts.google.com/gsi/style https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://accounts.google.com/gsi/ https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://accounts.google.com/gsi/ https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://accounts.google.com/gsi/client https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://accounts.google.com/gsi/style https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' http://127.0.0.1:43110 http://127.0.0.1:43111 http://127.0.0.1:43112 http://127.0.0.1:43113 http://127.0.0.1:43114 http://127.0.0.1:43115 http://127.0.0.1:43116 http://127.0.0.1:43117 http://127.0.0.1:43118 http://127.0.0.1:43119 https://accounts.google.com/gsi/ https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://accounts.google.com/gsi/ https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"

// UMQ(用户消息队列)模式常量
const (
Expand Down
11 changes: 11 additions & 0 deletions backend/internal/server/middleware/security_headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ var requiredCSPDirectiveValues = []struct {
{"style-src", AirwallexDemoStaticDomain},
{"style-src", AirwallexDemoCheckoutDomain},
{"frame-src", AirwallexDemoCheckoutDomain},
// @project-doc docs/interfaces/tf_cli_web_import.md#browser_security_headers
{"connect-src", "http://127.0.0.1:43110"},
{"connect-src", "http://127.0.0.1:43111"},
{"connect-src", "http://127.0.0.1:43112"},
{"connect-src", "http://127.0.0.1:43113"},
{"connect-src", "http://127.0.0.1:43114"},
{"connect-src", "http://127.0.0.1:43115"},
{"connect-src", "http://127.0.0.1:43116"},
{"connect-src", "http://127.0.0.1:43117"},
{"connect-src", "http://127.0.0.1:43118"},
{"connect-src", "http://127.0.0.1:43119"},
}

// GenerateNonce generates a cryptographically secure random nonce.
Expand Down
15 changes: 14 additions & 1 deletion backend/internal/server/middleware/security_headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package middleware

import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -346,7 +347,19 @@ func TestEnhanceCSPPolicy(t *testing.T) {
assert.Equal(t, 1, countDirectiveValue(enhanced, "worker-src", TencentCaptchaWorkerSource))
})

t.Run("default_policy_already_carries_tencent_captcha_domains", func(t *testing.T) {
t.Run("adds_only_the_tf_cli_loopback_ports", func(t *testing.T) {
policy := "default-src 'self'; connect-src 'self'"
enhanced := enhanceCSPPolicy(policy)

for port := 43110; port <= 43119; port++ {
origin := fmt.Sprintf("http://127.0.0.1:%d", port)
assert.Equal(t, 1, countDirectiveValue(enhanced, "connect-src", origin))
}
assert.NotContains(t, enhanced, "http://127.0.0.1:*")
assert.NotContains(t, enhanced, "http://localhost")
})

t.Run("default_policy_already_carries_required_domains", func(t *testing.T) {
// 默认策略与中间件强制注入表必须同形,否则 config.example.yaml 会误导自建用户
for _, required := range requiredCSPDirectiveValues {
assert.Equal(t, 1, countDirectiveValue(config.DefaultCSPPolicy, required.directive, required.value),
Expand Down
2 changes: 1 addition & 1 deletion deploy/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ security:
# 默认 CSP 策略(如果静态资源托管在其他域名,请自行覆盖)
# Note: __CSP_NONCE__ will be replaced with 'nonce-xxx' at request time for inline script security
# 注意:__CSP_NONCE__ 会在请求时被替换为 'nonce-xxx',用于内联脚本安全
policy: "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://accounts.google.com/gsi/client https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://accounts.google.com/gsi/style https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://accounts.google.com/gsi/ https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://accounts.google.com/gsi/ https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
policy: "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://accounts.google.com/gsi/client https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://accounts.google.com/gsi/style https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' http://127.0.0.1:43110 http://127.0.0.1:43111 http://127.0.0.1:43112 http://127.0.0.1:43113 http://127.0.0.1:43114 http://127.0.0.1:43115 http://127.0.0.1:43116 http://127.0.0.1:43117 http://127.0.0.1:43118 http://127.0.0.1:43119 https://accounts.google.com/gsi/ https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://accounts.google.com/gsi/ https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
proxy_probe:
# Allow skipping TLS verification for proxy probe (debug only)
# 允许代理探测时跳过 TLS 证书验证(仅用于调试)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@

- [架构](architecture/index.md):系统组成、进程启动、网关请求链路、账号调度和缓存一致性。读取时机:修改应用装配、服务边界、请求转发、调度快照或故障转移时读取。
- [领域](domains/index.md):身份租户、网关策略、计费额度、支付推广、内容审核和后台任务规则。读取时机:修改核心实体、业务不变量、状态转换、风险策略或结算规则时读取。
- [接口](interfaces/index.md):HTTP 路由、配置来源、账号能力矩阵、模型目录和六类上游平台适配契约。读取时机:修改外部 API、配置项、协议转换、错误响应或第三方集成时读取。
- [接口](interfaces/index.md):HTTP 路由、配置来源、浏览器回环导入、账号能力矩阵、模型目录和上游平台适配契约。读取时机:修改外部 API、配置项、协议转换、错误响应或第三方集成时读取。
- [运维](operations/index.md):部署迁移、账号/传输维护、监控告警、数据共享、开发流程和入口安全。读取时机:构建、发布、迁移、排障、数据维护或同步上游时读取。
2 changes: 1 addition & 1 deletion docs/interfaces/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Grok 文本转发有三项数据库运行时设置:`grok_default_text_model`

`gateway.grok` 属于启动时进程配置。`password_auth_enabled` 默认关闭并控制邮箱密码到 SSO/OAuth 的敏感入口;Free OAuth 本地软门禁由 `free_quota_soft_gate_enabled`、`free_quota_token_limit`、`free_quota_soft_gate_percent`、`free_quota_window_hours` 和 `free_quota_stats_cache_seconds` 控制。所有数值在启动时校验,修改后需要重启;统计缓存 miss 或查询故障按 fail-open 处理,但不能放宽 OAuth state 一次性消费、凭据持久化或 URL 信任边界。

验证码同样属于数据库运行时设置。Turnstile、腾讯天御与阿里云验证码 2.0 三者互斥。腾讯天御启用时必须同时具备正整数 `CaptchaAppId`、`AppSecretKey`、腾讯云 `SecretId` 和 `SecretKey`,并选择 `cn` 中国站或 `intl` 国际站;站点决定前端 SDK、构造函数形式、控制台入口和服务端票据校验 endpoint,`CaptchaAppId` 与云密钥必须来自同一站点,缺失或非法站点按 `cn` 回退。阿里云启用时必须具备 Scene ID、Prefix、AccessKey ID、AccessKey Secret 及 `cn` 或 `sgp` 地域。公开设置只返回各提供方的启用状态、站点和渲染所需的非敏感参数;管理响应只返回 secret 的“已配置”标记,空白更新保留原值,审计仅记录字段发生写入而不记录内容。腾讯与阿里云 Web SDK 所需的脚本、连接、iframe、worker 和样式来源由默认 CSP 与运行时 CSP 补全逻辑共同维护,覆盖自定义旧策略时也不能遗漏,其中阿里云静态资源允许 `https://*.alicdn.com`。Google GIS 同样由默认策略与旧自定义策略增强共同允许:`script-src` 仅加入 `https://accounts.google.com/gsi/client`,`frame-src`/`connect-src` 加入 `https://accounts.google.com/gsi/`,`style-src` 加入 `https://accounts.google.com/gsi/style`。
验证码同样属于数据库运行时设置。Turnstile、腾讯天御与阿里云验证码 2.0 三者互斥。腾讯天御启用时必须同时具备正整数 `CaptchaAppId`、`AppSecretKey`、腾讯云 `SecretId` 和 `SecretKey`,并选择 `cn` 中国站或 `intl` 国际站;站点决定前端 SDK、构造函数形式、控制台入口和服务端票据校验 endpoint,`CaptchaAppId` 与云密钥必须来自同一站点,缺失或非法站点按 `cn` 回退。阿里云启用时必须具备 Scene ID、Prefix、AccessKey ID、AccessKey Secret 及 `cn` 或 `sgp` 地域。公开设置只返回各提供方的启用状态、站点和渲染所需的非敏感参数;管理响应只返回 secret 的“已配置”标记,空白更新保留原值,审计仅记录字段发生写入而不记录内容。腾讯与阿里云 Web SDK 所需的脚本、连接、iframe、worker 和样式来源由默认 CSP 与运行时 CSP 补全逻辑共同维护,覆盖自定义旧策略时也不能遗漏,其中阿里云静态资源允许 `https://*.alicdn.com`。Google GIS 同样由默认策略与旧自定义策略增强共同允许:`script-src` 仅加入 `https://accounts.google.com/gsi/client`,`frame-src`/`connect-src` 加入 `https://accounts.google.com/gsi/`,`style-src` 加入 `https://accounts.google.com/gsi/style`。tf CLI 网页导入在 `connect-src` 中只允许 `http://127.0.0.1:43110` 到 `43119` 十个精确 Origin;代码默认策略、旧自定义策略增强和 `deploy/config.example.yaml` 必须同步,不能扩大为端口或局域网通配符。完整边界见 [tf CLI 网页导入](tf_cli_web_import.md)。

SMTP 的测试连接与实际发送共用同一建连路径和超时。`smtp_use_tls=true` 先按隐式 TLS 连接;仅当服务端以明文 SMTP 问候响应时改用强制 STARTTLS,服务端不支持升级时直接失败,不能明文发送认证。`smtp_use_tls=false` 保留机会式 STARTTLS,并在服务端不提供扩展时允许现有明文语义。两条路径都在认证成功后忽略非标准 QUIT 响应,因此后台连接测试与实际发信能力保持一致。

Expand Down
1 change: 1 addition & 0 deletions docs/interfaces/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- [HTTP 接口边界](http_api.md):公共、用户、管理员、支付和网关路由族及认证/错误边界。读取时机:新增或移动路由、调整中间件、认证方式或公共响应语义时读取。
- [配置边界](configuration.md):默认值、YAML、环境变量、数据库运行时设置和首次初始化之间的边界。读取时机:新增配置项、修改加载优先级、设置页面或部署变量时读取。
- [tf CLI 网页导入](tf_cli_web_import.md):Keys 页、本机回环协议、会话证明、双重确认和浏览器安全头。读取时机:修改 Keys 导入入口、URL fragment、localhost fetch、CSP 或 tf-cli 协议时读取。
- [上游账号能力矩阵](upstream_account_matrix.md):九个平台、七类账号和全部公开网关协议的正式支持、兼容保留与不支持边界。读取时机:新增平台/账号类型、修改创建导入校验、路由分派或能力承诺时读取。
- [API Key 上游用量查询](upstream_usage.md):API Key 账号的适配器、管理员查询接口、归一化结果和浏览器缓存边界。读取时机:修改 API Key 用量查询、适配器协议、账号用量展示或查询安全策略时读取。
- [Anthropic 上游](anthropic_upstream.md):OAuth、Setup Token、API Key、Bedrock、Vertex,以及 Messages/OpenAI 兼容转换和缓存/限流契约。读取时机:修改 Anthropic 认证、协议、beta、thinking、缓存或错误分类时读取。
Expand Down
53 changes: 53 additions & 0 deletions docs/interfaces/tf_cli_web_import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# tf CLI 网页导入

本文只记录 TokenRouter 对 [tf-cli 网页导入协议](https://github.com/TokenFlux/tf-cli/blob/main/docs/integrations/web-import.md) 的接入责任。端口、字段、错误码和签名格式以上游文档为准。

TokenRouter 后端不接收导入请求,也不新增授权接口。Keys 页把当前用户已加载到内存的 API Key 直接发给本机 `tf`;Key 不进入 URL、浏览器持久化、日志或分析事件。

<a id="session_fragment"></a>
## 会话片段

`tf login --from-web` 打开 `/keys#tf=1.<port>.<base64url-secret>`。`frontend/src/router/index.ts` 在 Router 创建和登录守卫运行前调用 `initializeTfCliImportSession()`:

- 任何 `#tf=` fragment 都立即通过 `history.replaceState` 删除,畸形值也删除。
- 只接受 Keys 路径、`43110` 至 `43119` 端口和 16 字节规范无 padding base64url secret。
- session 只在模块内存保留十分钟;过期、终端取消或请求被终端接受后清零。
- 同一 SPA 登录跳转可继续使用 session;刷新和外部认证整页跳转会丢失证明,之后只能走未验证兼容路径。

<a id="session_proof"></a>
## 回环协议

页面只访问 `http://127.0.0.1:43110` 至 `43119`。发现使用 `GET /ping`;有 session 时携带随机 challenge,并用 Web Crypto 校验 tf 返回的 HMAC。导入使用 `POST /import`,只在发现证明有效时发送 `X-TF-Session-Proof`。

页面只称“已验证当前 tf 会话”。proof 不验证网页、API Key、本机程序来源或后续网关结果,也不提供 freshness 或防重放。没有 session、proof 不匹配或 Web Crypto 不可用时仍可导入,但发送前必须显示未验证警告。若已验证后导入 proof 过期或计算失败,本次不发送 Key;页面先降级为未验证状态,再要求用户确认。

回环 fetch 固定使用 CORS、`credentials: omit`、`cache: no-store`、`redirect: error`、`referrerPolicy: no-referrer` 和 `targetAddressSpace: loopback`。发现总预算为 30 秒。浏览器自动完成 `OPTIONS` 预检,前端不手动发送。

## 字段映射

- `host` 使用页面 Origin。tf 校验同源后会恢复 CLI 自己的完整服务地址。
- `key_name` 取 Keys 页名称。tf 会保存该来源元数据;未显式指定本地名称时,合法值还会成为终端命名候选。
- 普通 Key 发送其 `group_id` 和 `group_name`。
- 复合 Key 不发送单一分组元数据;实际能力由 tf 查询模型目录识别。

HTTP `202 Accepted` 只表示终端已确认。tf 之后才校验网关,并在未显式指定名称时让用户选择自动识别、网页名称或自订名称;页面只能提示最终结果以终端为准,不能显示“导入成功”或“Key 已保存”。

<a id="user_confirmation"></a>
## 页面交互

Keys 行的更多菜单提供“导入 TF CLI”,不增加第二套入口或 Key 选择状态。弹窗先发现服务,再显示已验证状态或未验证警告;用户点击“发送到 TF CLI”后才发送 Key。POST 等待期间提示用户在终端核对来源并确认。网页确认决定是否发送 Key,终端确认决定 tf 是否继续处理,两者不能互相替代。

<a id="browser_security_headers"></a>
## 浏览器安全头

默认 CSP、旧自定义 CSP 的运行时补全和 `deploy/config.example.yaml` 都在 `connect-src` 中精确列出十个回环 Origin。不得扩大为端口通配符、`localhost` 或局域网地址。

TokenRouter 默认不限制 `local-network-access`。若反向代理自行设置 `Permissions-Policy`,必须允许顶层页面访问本地网络。现代 Chromium 首次访问可能请求本地网络权限;拒绝时页面显示未找到本机会话并允许重试。

## 验证

- `tfCliImport.spec.ts`:fragment、过期、固定 HMAC 向量、验证降级、请求选项和 proof。
- `TfCliImportDialog.spec.ts`、`KeysView.spec.ts`、`KeyActionMenu.spec.ts`:发送确认、状态文案、字段映射和菜单入口。
- `security_headers_test.go`:十个精确 CSP Origin 及默认策略同步。

相关文档:[接口目录](index.md)、[配置边界](configuration.md)、[复合 API Key](../domains/composite_api_keys.md)。
Loading