From f2b8a96c04f7315b8f2d1fceb2292bf890f1ce65 Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:36:35 +0800 Subject: [PATCH] fix(app): preserve passwords during Local startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local App startup saved passwords such as `好` or `a+b` correctly and started the service, but the launcher encoded its URL fragment twice. The browser then submitted a different password and stopped at the site-access screen. Both App launch URL paths now read the escaped fragment and serialize its query parameters once. Regression checks use the browser-visible fragment, which prevents an extra Go URL-decoding step from hiding the defect. ## Release notes ### 中文 - 修复 App 设置中文、空格或特殊字符作为本地站点密码后,启动时无法自动进入站点的问题。 ### English - Fixed automatic Local site entry when an App password contains Chinese text, spaces or special characters. ## Verification - Go App and Launcher checks passed, including password round trips, reopening a launch URL, empty passwords and retained language/theme parameters. - 105 relevant Web tests, type-checking and the Web build passed. - A development App built from the repaired source passed the actual fresh Launcher-to-Local flow with a one-character Chinese password: one automatic JSON login returned 200, Host UI opened without manual login, the consumed fragment was cleared, and Chinese/dark preferences were retained. Compatible App bug fix; signaling, Native control, capture and persistent room contracts are unchanged. --- internal/app/app.go | 12 ++++++------ internal/app/app_test.go | 42 +++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index b5c5d2e..255d1cd 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -503,13 +503,13 @@ func launchURL(raw string) string { if err != nil { return raw } - fragment, err := url.ParseQuery(parsed.Fragment) + fragment, err := url.ParseQuery(parsed.EscapedFragment()) if err != nil { return raw } fragment.Set("piik-client", "1") - parsed.Fragment = fragment.Encode() - return parsed.String() + parsed.Fragment, parsed.RawFragment = "", "" + return parsed.String() + "#" + fragment.Encode() } func launchURLWithLocalAccess(raw, password string) string { @@ -517,7 +517,7 @@ func launchURLWithLocalAccess(raw, password string) string { if err != nil { return raw } - fragment, err := url.ParseQuery(parsed.Fragment) + fragment, err := url.ParseQuery(parsed.EscapedFragment()) if err != nil { return launchURL(raw) } @@ -526,8 +526,8 @@ func launchURLWithLocalAccess(raw, password string) string { fragment.Set("client-access", password) } fragment.Set("piik-client", "1") - parsed.Fragment = fragment.Encode() - return parsed.String() + parsed.Fragment, parsed.RawFragment = "", "" + return parsed.String() + "#" + fragment.Encode() } type nativeRuntime struct { diff --git a/internal/app/app_test.go b/internal/app/app_test.go index fbf57ae..6ce4526 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -94,7 +94,7 @@ func TestAppLaunchURLMarksThePageWithoutChangingOrigin(t *testing.T) { parsed.RawQuery != "" { t.Fatalf("launch URL = %q, %v", value, err) } - fragment, err := url.ParseQuery(parsed.Fragment) + fragment, err := url.ParseQuery(parsed.EscapedFragment()) if err != nil || fragment.Get("piik-client") != "1" { t.Fatalf("App launch fragment = %q, %v", parsed.Fragment, err) } @@ -192,7 +192,7 @@ func TestLaunchURLPreservesLocalAccessInsideThePrivateFragment(t *testing.T) { if err != nil || parsed.RawQuery != "" { t.Fatalf("local native launch URL = %q, %v", value, err) } - fragment, err := url.ParseQuery(parsed.Fragment) + fragment, err := url.ParseQuery(parsed.EscapedFragment()) if err != nil || fragment.Get("client-access") != "secret" || fragment.Get("piik-client") != "1" { t.Fatalf("local native launch fragment = %q, %v", parsed.Fragment, err) @@ -200,26 +200,36 @@ func TestLaunchURLPreservesLocalAccessInsideThePrivateFragment(t *testing.T) { } func TestLaunchURLEncodesAndClearsOptionalLocalAccess(t *testing.T) { - value := launchURLWithLocalAccess( - "http://localhost:8787/#retained=yes&client-access=old", - " 中文 a+b&c?d=e ", - ) - parsed, err := url.Parse(value) - if err != nil { - t.Fatal(err) - } - fragment, err := url.ParseQuery(parsed.Fragment) - if err != nil || fragment.Get("client-access") != " 中文 a+b&c?d=e " || - fragment.Get("piik-client") != "1" || fragment.Get("retained") != "yes" { - t.Fatalf("encoded local launch fragment = %q, %v", parsed.Fragment, err) + for _, password := range []string{"x", "好", " ", "+", "%", "#&?=", " 中文 a+b&c?d=e% "} { + t.Run(password, func(t *testing.T) { + value := launchURLWithLocalAccess( + "http://localhost:8787/?retained=query#retained=a%2Bb%25&client-access=old&piik-lang=zh&piik-mode=text&piik-theme=dark", + password, + ) + for _, target := range []string{value, launchURL(value)} { + parsed, err := url.Parse(target) + if err != nil || parsed.RawQuery != "retained=query" { + t.Fatalf("local launch URL = %q, %v", target, err) + } + // location.hash stays escaped. Parsing Fragment instead would decode + // twice and hide the browser's percent-encoded-password regression. + fragment, err := url.ParseQuery(parsed.EscapedFragment()) + if err != nil || fragment.Get("client-access") != password || + fragment.Get("piik-client") != "1" || fragment.Get("retained") != "a+b%" || + fragment.Get("piik-lang") != "zh" || fragment.Get("piik-mode") != "text" || + fragment.Get("piik-theme") != "dark" { + t.Fatalf("browser-visible launch fragment = %q, %v", parsed.EscapedFragment(), err) + } + } + }) } open := launchURLWithLocalAccess("http://localhost:8787/#client-access=old", "") - parsed, err = url.Parse(open) + parsed, err := url.Parse(open) if err != nil { t.Fatal(err) } - fragment, err = url.ParseQuery(parsed.Fragment) + fragment, err := url.ParseQuery(parsed.EscapedFragment()) if err != nil || fragment.Get("client-access") != "" || fragment.Get("piik-client") != "1" { t.Fatalf("open local launch fragment = %q, %v", parsed.Fragment, err) }