From 26dd84abaabe7d913d629a333969b37388a54515 Mon Sep 17 00:00:00 2001 From: mrlee <470349233@qq.com> Date: Mon, 28 Sep 2026 17:48:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(cloudflare):=20Token=20=E8=A7=A3=E7=A0=81?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=20URL-safe=20base64=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E5=B8=83=20v1.6.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 14 +++++++++++++ VERSION | 2 +- docs/README.md | 2 +- docs/system-upgrade.md | 8 ++++---- internal/cloudflared/config.go | 31 ++++++++++++++++++++++++----- internal/cloudflared/config_test.go | 23 +++++++++++++++++++++ web/package-lock.json | 4 ++-- web/package.json | 2 +- web/src/views/ExampleView.vue | 16 +++++++-------- 9 files changed, 80 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c64d7e8..28a288f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. +## [v1.6.6] - 2026-09-28 + +### 修复 + +- **Token 解码兼容 URL-safe base64**:Cloudflare 面板生成的 Tunnel Token 使用 URL-safe base64(含 `-` / `_`),原先只按标准 base64 解码,会报 `illegal base64 data at input byte N` 并返回 502。现在同时兼容标准 / URL-safe 与有无填充四种形式,失败时给出更明确的提示 + +### 变更 + +- 版本号提升到 1.6.6;`AgentVersion` 保持 **0.14.1** + +### 测试 + +- 新增 URL-safe 与无填充 token 解码测试 + ## [v1.6.5] - 2026-09-28 ### 新增 diff --git a/VERSION b/VERSION index 9f05f9f..ec70f75 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.5 +1.6.6 diff --git a/docs/README.md b/docs/README.md index 9e64674..b90057a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ # Fonu 文档索引 > 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。 -> 最近更新:2026-09-28(v1.6.5) +> 最近更新:2026-09-28(v1.6.6) ## 方案与设计(本目录) diff --git a/docs/system-upgrade.md b/docs/system-upgrade.md index 5cc72d6..cff18ca 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.6.5 的验证流程: +示例页面用于验证升级结果。v1.6.6 的验证流程: 1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。 -2. 确认远端 `main` 分支的 `VERSION` 为 `1.6.5`。 +2. 确认远端 `main` 分支的 `VERSION` 为 `1.6.6`。 3. 在侧栏看到「一键升级」后点击并确认。 4. 等待升级弹窗显示成功,页面自动刷新。 5. 打开侧栏「示例页面」,确认: - 页面路由为 `/example`; - - 页面显示的版本为 `1.6.5`; - - 页面显示验证标识 `v1.6.5-example-page`; + - 页面显示的版本为 `1.6.6`; + - 页面显示验证标识 `v1.6.6-example-page`; - 页面显示独立的「本次升级验证」区块; - 「升级验证」显示为「通过」。 diff --git a/internal/cloudflared/config.go b/internal/cloudflared/config.go index 9fe85a9..ec322d3 100644 --- a/internal/cloudflared/config.go +++ b/internal/cloudflared/config.go @@ -20,12 +20,9 @@ func DecodeToken(token string) (accountTag, tunnelID, tunnelSecret string, err e if clean == "" { return "", "", "", fmt.Errorf("token 为空") } - if remainder := len(clean) % 4; remainder != 0 { - clean += strings.Repeat("=", 4-remainder) - } - raw, err := base64.StdEncoding.DecodeString(clean) + raw, err := decodeBase64(clean) if err != nil { - return "", "", "", fmt.Errorf("token base64 解码失败: %w", err) + return "", "", "", err } var payload tokenPayload if err := json.Unmarshal(raw, &payload); err != nil || payload.TunnelID == "" { @@ -45,6 +42,30 @@ func DecodeToken(token string) (accountTag, tunnelID, tunnelSecret string, err e return payload.AccountTag, payload.TunnelID, payload.TunnelSecret, nil } +// decodeBase64 同时兼容标准 base64 与 URL-safe base64,并兼容有无填充两种形式。 +// Cloudflare 面板生成的 Tunnel Token 使用 URL-safe 编码,直接用标准 base64 会在 '-' / '_' 处失败。 +func decodeBase64(input string) ([]byte, error) { + encodings := []*base64.Encoding{ + base64.StdEncoding, base64.RawStdEncoding, + base64.URLEncoding, base64.RawURLEncoding, + } + for _, encoding := range encodings { + if data, err := encoding.DecodeString(input); err == nil { + return data, nil + } + } + padded := input + if remainder := len(padded) % 4; remainder != 0 { + padded += strings.Repeat("=", 4-remainder) + } + for _, encoding := range encodings { + if data, err := encoding.DecodeString(padded); err == nil { + return data, nil + } + } + return nil, fmt.Errorf("token base64 解码失败:请确认粘贴的是完整的 Cloudflare 隧道 token") +} + // CredentialsJSON 生成 cloudflared 需要的 creds.json 内容。 func CredentialsJSON(accountTag, tunnelID, tunnelSecret string) string { data, _ := json.MarshalIndent(map[string]string{ diff --git a/internal/cloudflared/config_test.go b/internal/cloudflared/config_test.go index 0e0e392..04aee21 100644 --- a/internal/cloudflared/config_test.go +++ b/internal/cloudflared/config_test.go @@ -1,6 +1,7 @@ package cloudflared import ( + "bytes" "encoding/base64" "strings" "testing" @@ -32,6 +33,28 @@ func TestDecodeTokenRejectsInvalid(t *testing.T) { } } +func TestDecodeTokenURLSafe(t *testing.T) { + payload := `{"a":"account-1","t":"tunnel-2","s":"secret-3"}` + encoded := base64.RawURLEncoding.EncodeToString([]byte(payload)) + if _, _, _, err := DecodeToken(encoded); err != nil { + t.Fatalf("URL-safe token 应可解码: %v", err) + } +} + +func TestDecodeBase64Flexible(t *testing.T) { + raw := []byte{0xfb, 0xef, 0xbe, 0xad} + for _, encoded := range []string{ + base64.RawURLEncoding.EncodeToString(raw), + base64.URLEncoding.EncodeToString(raw), + base64.RawStdEncoding.EncodeToString(raw), + } { + got, err := decodeBase64(encoded) + if err != nil || !bytes.Equal(got, raw) { + t.Fatalf("编码 %q 解码失败:%v", encoded, err) + } + } +} + func TestBuildIngressInvariants(t *testing.T) { got := BuildIngress([]string{"B.example.com", "a.example.com", "a.example.com", " "}, "http://127.0.0.1:80") if len(got) != 3 { diff --git a/web/package-lock.json b/web/package-lock.json index 0bf5179..af4ed4a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "fonu-web", - "version": "1.6.5", + "version": "1.6.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fonu-web", - "version": "1.6.5", + "version": "1.6.6", "dependencies": { "@codemirror/commands": "^6.11.1", "@codemirror/lang-javascript": "^6.2.5", diff --git a/web/package.json b/web/package.json index 2e35bde..cec40de 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "fonu-web", "private": true, - "version": "1.6.5", + "version": "1.6.6", "packageManager": "npm@10.9.4", "type": "module", "scripts": { diff --git a/web/src/views/ExampleView.vue b/web/src/views/ExampleView.vue index 72d6d54..a53b136 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.6.5。 + 看到这个新区块,说明前端资源已经从旧版本切换到 v1.6.6。
@@ -33,16 +33,16 @@ - +
    -
  • 页面版本提示更新为 v1.6.5。
  • -
  • 「本次升级验证」区块和页面状态同步更新为 v1.6.5。
  • -
  • 验证标识更新为 v1.6.5-example-page。
  • +
  • 页面版本提示更新为 v1.6.6。
  • +
  • 「本次升级验证」区块和页面状态同步更新为 v1.6.6。
  • +
  • 验证标识更新为 v1.6.6-example-page。
@@ -87,8 +87,8 @@ const message = useMessage() const version = ref(__APP_VERSION__) const checkedAt = ref('') const refreshing = ref(false) -const verificationRound = 'v1.6.5' -const verificationMarker = 'v1.6.5-example-page' +const verificationRound = 'v1.6.6' +const verificationMarker = 'v1.6.6-example-page' async function refreshVersion() { refreshing.value = true