diff --git a/CHANGELOG.md b/CHANGELOG.md index cca29a7..1f78052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. +## [v1.4.3] - 2026-09-28 + +### 修复 + +- **HTTPS 直连旧地址自愈**:从 SSH 隧道切换到「HTTPS + 证书固定」后,历史实例的 `agent_url` 可能仍为已失效的 `http://127.0.0.1:17701`,隧道关闭后直接报 `connection refused`。现在读取 HTTPS transport 时会依据 `ssh_host` 与 `agent_listen_addr` 自动重建 `https://:`,并在地址变化时回写数据库;切回 SSH 隧道也使用同一重建逻辑,无需手工修改地址 + +### 变更 + +- 版本号提升到 1.4.3;`web/package.json` 与 `web/package-lock.json` 同步对齐 +- `AgentVersion` 保持 0.13.0 + +### 测试 + +- `internal/frp`:新增 `TestAgentHTTPSBaseURL`,覆盖旧隧道地址重建、已是 HTTPS 地址保持、默认端口回退与缺少 SSH 主机的情况 + ## [v1.4.2] - 2026-09-28 ### 修复 diff --git a/VERSION b/VERSION index 9df886c..428b770 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.2 +1.4.3 diff --git a/docs/README.md b/docs/README.md index 66af479..39e2e0b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ # Fonu 文档索引 > 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。 -> 最近更新:2026-09-28(v1.4.2) +> 最近更新:2026-09-28(v1.4.3) ## 方案与设计(本目录) diff --git a/docs/system-upgrade.md b/docs/system-upgrade.md index 1537f67..db3510e 100644 --- a/docs/system-upgrade.md +++ b/docs/system-upgrade.md @@ -238,16 +238,16 @@ FONU_VERSION=<旧版本> docker compose up -d --no-deps --force-recreate fonu ## 10. 在线升级验证 -示例页面用于验证升级结果。v1.4.2 的验证流程: +示例页面用于验证升级结果。v1.4.3 的验证流程: 1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。 -2. 确认远端 `main` 分支的 `VERSION` 为 `1.4.2`。 +2. 确认远端 `main` 分支的 `VERSION` 为 `1.4.3`。 3. 在侧栏看到「一键升级」后点击并确认。 4. 等待升级弹窗显示成功,页面自动刷新。 5. 打开侧栏「示例页面」,确认: - 页面路由为 `/example`; - - 页面显示的版本为 `1.4.2`; - - 页面显示验证标识 `v1.4.2-example-page`; + - 页面显示的版本为 `1.4.3`; + - 页面显示验证标识 `v1.4.3-example-page`; - 页面显示独立的「本次升级验证」区块; - 「升级验证」显示为「通过」。 diff --git a/internal/frp/agent_https_test.go b/internal/frp/agent_https_test.go index 9da1dbc..1be05e0 100644 --- a/internal/frp/agent_https_test.go +++ b/internal/frp/agent_https_test.go @@ -46,3 +46,39 @@ func TestPinTLSConfigVerification(t *testing.T) { t.Fatal("错误 pin 应校验失败") } } + +func TestAgentHTTPSBaseURL(t *testing.T) { + cases := []struct { + name string + server Server + want string + }{ + { + name: "旧隧道地址按监听端口重建", + server: Server{AgentURL: "http://127.0.0.1:17701", SSHHost: "agent.example.com", AgentListenAddr: "0.0.0.0:7443"}, + want: "https://agent.example.com:7443", + }, + { + name: "已是 HTTPS 地址保持不变", + server: Server{AgentURL: "https://agent.example.com:8443", SSHHost: "agent.example.com", AgentListenAddr: "0.0.0.0:7443"}, + want: "https://agent.example.com:8443", + }, + { + name: "监听地址缺少端口时使用默认端口", + server: Server{AgentURL: "http://127.0.0.1:17701", SSHHost: "agent.example.com"}, + want: "https://agent.example.com:7443", + }, + { + name: "缺少 SSH 主机时保留原地址", + server: Server{AgentURL: "http://127.0.0.1:17701"}, + want: "http://127.0.0.1:17701", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := agentHTTPSBaseURL(tc.server); got != tc.want { + t.Fatalf("agentHTTPSBaseURL() = %q, 期望 %q", got, tc.want) + } + }) + } +} diff --git a/internal/frp/service.go b/internal/frp/service.go index bc95350..b4ebe24 100644 --- a/internal/frp/service.go +++ b/internal/frp/service.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "strings" "sync" "time" @@ -1009,7 +1010,7 @@ func (s *Service) SwitchAgentToTunnel(ctx context.Context, serverID int64) (Serv if err != nil { return Server{}, err } - client := NewAgentClientWithPin(server.AgentURL, token, server.AgentTLSPin) + client := NewAgentClientWithPin(agentHTTPSBaseURL(server), token, server.AgentTLSPin) if err := client.DisableHTTPS(ctx); err != nil { return Server{}, fmt.Errorf("关闭 HTTPS 直连失败: %w", err) } @@ -1595,6 +1596,24 @@ func (s *Service) invalidateAgentClient(serverID int64) { s.agentMu.Unlock() } +func agentHTTPSBaseURL(server Server) string { + current := strings.TrimRight(strings.TrimSpace(server.AgentURL), "/") + if strings.HasPrefix(strings.ToLower(current), "https://") { + return current + } + host := strings.TrimSpace(server.SSHHost) + if host == "" { + return current + } + port := 7443 + if _, rawPort, err := net.SplitHostPort(strings.TrimSpace(server.AgentListenAddr)); err == nil { + if parsed, err := strconv.Atoi(rawPort); err == nil && parsed > 0 { + port = parsed + } + } + return fmt.Sprintf("https://%s:%d", host, port) +} + func (s *Service) agentClient(ctx context.Context, serverID int64) (*AgentClient, bool) { server, err := s.store.GetServer(ctx, serverID) if err != nil || !server.MgmtEnabled || !server.AgentConfigured { @@ -1618,9 +1637,20 @@ func (s *Service) agentClient(ctx context.Context, serverID int64) (*AgentClient baseURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.localPort) } else if server.AgentTransport == "https-pin" { pin = strings.TrimSpace(server.AgentTLSPin) - if baseURL == "" || pin == "" { + if pin == "" { return nil, false } + baseURL = agentHTTPSBaseURL(server) + if !strings.HasPrefix(strings.ToLower(baseURL), "https://") { + return nil, false + } + if baseURL != strings.TrimRight(strings.TrimSpace(server.AgentURL), "/") { + err := s.store.SetAgentTransport(ctx, serverID, baseURL, "https-pin", server.AgentLocalPort, + pin, server.AgentListenAddr, server.AgentTransportState, server.AgentTransportError) + if err != nil { + s.logger.Warn("agent https url repair failed", "server_id", serverID, "error", err) + } + } } else if baseURL == "" { return nil, false } diff --git a/web/package-lock.json b/web/package-lock.json index ae1e75b..3d6c64e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "fonu-web", - "version": "1.4.2", + "version": "1.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fonu-web", - "version": "1.4.2", + "version": "1.4.3", "dependencies": { "@codemirror/commands": "^6.11.1", "@codemirror/lang-javascript": "^6.2.5", diff --git a/web/package.json b/web/package.json index c82f4db..4681259 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "fonu-web", "private": true, - "version": "1.4.2", + "version": "1.4.3", "packageManager": "npm@10.9.4", "type": "module", "scripts": { diff --git a/web/src/views/ExampleView.vue b/web/src/views/ExampleView.vue index b86a824..5b2415b 100644 --- a/web/src/views/ExampleView.vue +++ b/web/src/views/ExampleView.vue @@ -8,7 +8,7 @@ - + 当前页面由 v{{ version }} 提供,验证标识为 {{ verificationMarker }}。 如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。 @@ -19,7 +19,7 @@ {{ verificationRound }}
- 看到这个新区块,说明前端资源已经从旧版本切换到 v1.4.2。 + 看到这个新区块,说明前端资源已经从旧版本切换到 v1.4.3。
@@ -33,16 +33,16 @@ - +
    -
  • 页面版本提示更新为 v1.4.2。
  • -
  • 「本次升级验证」区块和页面状态同步更新为 v1.4.2。
  • -
  • 验证标识更新为 v1.4.2-example-page。
  • +
  • 页面版本提示更新为 v1.4.3。
  • +
  • 「本次升级验证」区块和页面状态同步更新为 v1.4.3。
  • +
  • 验证标识更新为 v1.4.3-example-page。
@@ -87,8 +87,8 @@ const message = useMessage() const version = ref(__APP_VERSION__) const checkedAt = ref('') const refreshing = ref(false) -const verificationRound = 'v1.4.2' -const verificationMarker = 'v1.4.2-example-page' +const verificationRound = 'v1.4.3' +const verificationMarker = 'v1.4.3-example-page' async function refreshVersion() { refreshing.value = true