fix(app): preserve passwords during Local startup

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.
This commit is contained in:
AAA authored and GitHub committed 2026-09-13 01:36:35 +08:00
1 parent 322e5d91b2
commit f2b8a96c04
2 files changed
+32 -22

No files matched your search

+6 -6
View File
@@ -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 {
+26 -16
View File
@@ -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)
}