mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-15 07:40:59 +00:00
fix(sub): use a fullwidth percent in USAGE_PERCENTAGE (#6174)
* fix(sub): use a fullwidth percent in USAGE_PERCENTAGE A remark is placed in the share link fragment, so an ASCII percent is percent-encoded to %25. Happ treats such a fragment as malformed, discards the whole remark and falls back to showing the server hostname, which defeats the point of a remark template and leaks the host into the client's server list. Emit U+FF05 FULLWIDTH PERCENT SIGN instead. It renders the same to a reader, never produces %25, and round-trips through url.Parse unchanged. * test(sub): exercise production fragment encoding --------- Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com>
This commit is contained in:
@@ -39,7 +39,7 @@ export const REMARK_VARIABLES: RemarkVar[] = [
|
||||
{ token: 'STATUS_EMOJI', group: 'time', sample: '✅' },
|
||||
{ token: 'DAYS_LEFT', group: 'time', sample: '12' },
|
||||
{ token: 'TIME_LEFT', group: 'time', sample: '12d 4h 30m' },
|
||||
{ token: 'USAGE_PERCENTAGE', group: 'time', sample: '52.3%' },
|
||||
{ token: 'USAGE_PERCENTAGE', group: 'time', sample: '52.3%' },
|
||||
{ token: 'EXPIRE_DATE', group: 'time', sample: '2026-09-01' },
|
||||
{ token: 'JALALI_EXPIRE_DATE', group: 'time', sample: '1405/06/10' },
|
||||
{ token: 'EXPIRE_UNIX', group: 'time', sample: '1788300000' },
|
||||
|
||||
@@ -350,8 +350,8 @@ func statusEmoji(st xray.ClientTraffic) string {
|
||||
}
|
||||
}
|
||||
|
||||
// usagePercentage computes the traffic usage as a percentage string (e.g. "52.3%").
|
||||
// Returns "" when the client has no traffic limit.
|
||||
// usagePercentage computes the traffic usage as a percentage string (e.g. "52.3%").
|
||||
// Uses U+FF05: an ASCII percent encodes to %25, which Happ rejects, dropping the remark.
|
||||
func usagePercentage(st xray.ClientTraffic) string {
|
||||
if st.Total <= 0 {
|
||||
return ""
|
||||
@@ -361,7 +361,7 @@ func usagePercentage(st xray.ClientTraffic) string {
|
||||
if pct > 100 {
|
||||
pct = 100 // clamp over-quota usage, consistent with TRAFFIC_LEFT
|
||||
}
|
||||
return fmt.Sprintf("%.1f%%", pct)
|
||||
return fmt.Sprintf("%.1f%", pct)
|
||||
}
|
||||
|
||||
// timeLeftLabel renders remaining time as "Xd Xh Xm" (or shorter when days/hours
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -480,18 +481,33 @@ func TestStatusEmoji(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUsagePercentage(t *testing.T) {
|
||||
if got := usagePercentage(xray.ClientTraffic{Total: 100 * gb, Up: 25 * gb, Down: 25 * gb}); got != "50.0%" {
|
||||
if got := usagePercentage(xray.ClientTraffic{Total: 100 * gb, Up: 25 * gb, Down: 25 * gb}); got != "50.0%" {
|
||||
t.Errorf("usagePercentage 50%% = %q", got)
|
||||
}
|
||||
if got := usagePercentage(xray.ClientTraffic{Total: 0}); got != "" {
|
||||
t.Errorf("usagePercentage unlimited = %q, want empty", got)
|
||||
}
|
||||
if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 10 * gb}); got != "100.0%" {
|
||||
if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 10 * gb}); got != "100.0%" {
|
||||
t.Errorf("usagePercentage 100%% = %q", got)
|
||||
}
|
||||
// Over-quota usage clamps to 100%, consistent with TRAFFIC_LEFT.
|
||||
if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 25 * gb}); got != "100.0%" {
|
||||
t.Errorf("usagePercentage over-quota = %q, want 100.0%%", got)
|
||||
if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 25 * gb}); got != "100.0%" {
|
||||
t.Errorf("usagePercentage over-quota = %q, want 100.0%", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsagePercentageSurvivesFragmentEncoding(t *testing.T) {
|
||||
remark := "node " + usagePercentage(xray.ClientTraffic{Total: 100 * gb, Up: 50 * gb})
|
||||
link := buildLinkWithParams("vless://id@example.test:443", nil, remark)
|
||||
if strings.Contains(link, "%25") {
|
||||
t.Fatalf("encoded remark contains %%25, Happ drops such remarks: %s", link)
|
||||
}
|
||||
u, err := url.Parse(link)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if u.Fragment != remark {
|
||||
t.Fatalf("fragment = %q, want %q", u.Fragment, remark)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,7 +562,7 @@ func TestExpandNewTokensInTemplate(t *testing.T) {
|
||||
|
||||
cases := []struct{ tmpl, want string }{
|
||||
{"{{STATUS_EMOJI}}", "✅"},
|
||||
{"{{USAGE_PERCENTAGE}}", "50.0%"},
|
||||
{"{{USAGE_PERCENTAGE}}", "50.0%"},
|
||||
{"{{PROTOCOL}}", "VLESS"},
|
||||
{"{{TRANSPORT}}", "ws"},
|
||||
{"{{SECURITY}}", "REALITY"},
|
||||
@@ -619,7 +635,7 @@ func TestExpandRemarkVars_SingleBracketUI(t *testing.T) {
|
||||
{"{DATA_USAGE}", "50.00GB"},
|
||||
{"{DATA_LIMIT}", "100.00GB"},
|
||||
{"{STATUS_EMOJI}", "✅"},
|
||||
{"{USAGE_PERCENTAGE}", "50.0%"},
|
||||
{"{USAGE_PERCENTAGE}", "50.0%"},
|
||||
{"{PROTOCOL}", "VLESS"},
|
||||
{"{TRANSPORT}", "ws"},
|
||||
{"{SECURITY}", "TLS"},
|
||||
|
||||
Reference in New Issue
Block a user