Bläddra i källkod

fix(sub): default https:// for scheme-less support and profile URLs

A support URL saved without a scheme (e.g. "t.me/handle") is served
verbatim in the subscription Support-Url header and page data, and client
apps resolve it relative to the subscription domain — clicking it lands
on "https://panel.example/t.me/handle". Same hazard for the profile URL.

Default the scheme to https:// when none is present, both when saving the
settings and when reading already-stored values, so existing databases are
covered without a migration. Deliberate non-http schemes (tg://, mailto:,
tel:) pass through untouched, which is why these two fields don't go
through SanitizeHTTPURL's http(s)-only validation.

Closes #5738
MHSanaei 23 timmar sedan
förälder
incheckning
fb3a1559b2
3 ändrade filer med 60 tillägg och 2 borttagningar
  1. 21 0
      internal/util/common/url.go
  2. 29 0
      internal/util/common/url_test.go
  3. 10 2
      internal/web/service/setting.go

+ 21 - 0
internal/util/common/url.go

@@ -0,0 +1,21 @@
+package common
+
+import "strings"
+
+// EnsureURLScheme prepends https:// to a URL that carries no scheme, so
+// subscription apps and browsers don't resolve it relative to the panel's own
+// domain (e.g. "t.me/support" turning into "https://panel.example/t.me/support").
+// Values with an explicit scheme (https://, tg://, mailto:, tel:) and empty
+// strings pass through untouched.
+func EnsureURLScheme(raw string) string {
+	trimmed := strings.TrimSpace(raw)
+	if trimmed == "" {
+		return ""
+	}
+	if strings.Contains(trimmed, "://") ||
+		strings.HasPrefix(trimmed, "mailto:") ||
+		strings.HasPrefix(trimmed, "tel:") {
+		return trimmed
+	}
+	return "https://" + trimmed
+}

+ 29 - 0
internal/util/common/url_test.go

@@ -0,0 +1,29 @@
+package common
+
+import "testing"
+
+func TestEnsureURLScheme(t *testing.T) {
+	tests := []struct {
+		name string
+		in   string
+		want string
+	}{
+		{"empty", "", ""},
+		{"whitespace only", "   ", ""},
+		{"bare telegram handle", "t.me/xui_support", "https://t.me/xui_support"},
+		{"bare domain with path", "example.com/help", "https://example.com/help"},
+		{"already https", "https://t.me/xui_support", "https://t.me/xui_support"},
+		{"already http", "http://example.com", "http://example.com"},
+		{"telegram deep link", "tg://resolve?domain=xui_support", "tg://resolve?domain=xui_support"},
+		{"mailto", "mailto:[email protected]", "mailto:[email protected]"},
+		{"tel", "tel:+1234567890", "tel:+1234567890"},
+		{"trims whitespace", "  t.me/xui_support  ", "https://t.me/xui_support"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := EnsureURLScheme(tt.in); got != tt.want {
+				t.Errorf("EnsureURLScheme(%q) = %q, want %q", tt.in, got, tt.want)
+			}
+		})
+	}
+}

+ 10 - 2
internal/web/service/setting.go

@@ -710,11 +710,13 @@ func (s *SettingService) GetSubTitle() (string, error) {
 }
 
 func (s *SettingService) GetSubSupportUrl() (string, error) {
-	return s.getString("subSupportUrl")
+	value, err := s.getString("subSupportUrl")
+	return common.EnsureURLScheme(value), err
 }
 
 func (s *SettingService) GetSubProfileUrl() (string, error) {
-	return s.getString("subProfileUrl")
+	value, err := s.getString("subProfileUrl")
+	return common.EnsureURLScheme(value), err
 }
 
 func (s *SettingService) GetSubAnnounce() (string, error) {
@@ -1177,6 +1179,12 @@ func validateSettingsURLs(allSetting *entity.AllSetting) error {
 		}
 		allSetting.TgBotAPIServer = u
 	}
+	// Support/profile links land in subscription headers and page data, where
+	// client apps resolve a scheme-less value against the panel's own domain.
+	// Non-http schemes (tg://, mailto:) are legitimate here, so only default
+	// the scheme instead of forcing SanitizeHTTPURL's http(s)-only rule.
+	allSetting.SubSupportUrl = common.EnsureURLScheme(allSetting.SubSupportUrl)
+	allSetting.SubProfileUrl = common.EnsureURLScheme(allSetting.SubProfileUrl)
 	return nil
 }