mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
1d85ef138e
* fix(sub): prevent default profile page URL disclosure Add explicit none, builtin, and custom profile page modes. Preserve existing custom URLs and warn before exposing the built-in page. Cover mode selection, legacy settings, and subscription response headers. * fix(subscription): add profile page link options and upgrade notes
194 lines
6.0 KiB
Go
194 lines
6.0 KiB
Go
package sub
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
|
)
|
|
|
|
func TestRenderSubPlaceholders(t *testing.T) {
|
|
data := subPlaceholderData{
|
|
SubID: "sub-123",
|
|
Context: remarkContext{client: model.Client{
|
|
Email: "Ilnur",
|
|
ID: "abcdef12-3456-7890-abcd-ef1234567890",
|
|
SubID: "sub-123",
|
|
TgID: 42,
|
|
Enable: true,
|
|
}},
|
|
HasCtx: true,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
tmpl string
|
|
data subPlaceholderData
|
|
want string
|
|
}{
|
|
{
|
|
name: "identity tokens",
|
|
tmpl: "{{EMAIL}}/{{ID}}/{{SHORT_ID}}/{{SUB_ID}}/{{TELEGRAM_ID}}",
|
|
data: data,
|
|
want: "Ilnur/abcdef12-3456-7890-abcd-ef1234567890/abcdef12/sub-123/42",
|
|
},
|
|
{
|
|
name: "no template",
|
|
tmpl: "isVPN",
|
|
data: subPlaceholderData{SubID: "sub-123"},
|
|
want: "isVPN",
|
|
},
|
|
{
|
|
name: "unsupported tokens stay literal",
|
|
tmpl: "{{SUB_ID}}/{{INBOUND}}/{{TRAFFIC_LEFT}}/{{PROTOCOL}}/{EMAIL}",
|
|
data: subPlaceholderData{SubID: "sub-123"},
|
|
want: "sub-123/{{INBOUND}}/{{TRAFFIC_LEFT}}/{{PROTOCOL}}/{EMAIL}",
|
|
},
|
|
{
|
|
name: "URL values are escaped",
|
|
tmpl: "https://support.example/?email={{EMAIL}}&sub={{SUB_ID}}",
|
|
data: subPlaceholderData{
|
|
SubID: "sub id",
|
|
Context: remarkContext{client: model.Client{
|
|
Email: "john doe@example.com",
|
|
SubID: "sub id",
|
|
}},
|
|
HasCtx: true,
|
|
Escape: true,
|
|
},
|
|
want: "https://support.example/?email=john+doe%40example.com&sub=sub+id",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := renderSubPlaceholders(tt.tmpl, tt.data); got != tt.want {
|
|
t.Fatalf("renderSubPlaceholders() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMetadataForSubRequestOmitsUnconfiguredProfileURL(t *testing.T) {
|
|
a := &SUBController{
|
|
subTitle: "isVPN",
|
|
subSupportUrl: "https://support.example/",
|
|
}
|
|
metadata := a.metadataForSubRequest(func() *SubService {
|
|
t.Fatal("metadataForSubRequest loaded a subscription context without configured placeholders")
|
|
return nil
|
|
}, "sub-123", "https://sub.example/sub-123")
|
|
|
|
if metadata.ProfileURL != "" {
|
|
t.Fatalf("ProfileURL = %q, want no link when unconfigured", metadata.ProfileURL)
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionProfileURLRequiresExplicitConfiguration(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
initSubDB(t)
|
|
seedInfoEndpointSub(t, "profile-sub", "profile@example.com")
|
|
|
|
for _, config := range []struct {
|
|
name, profileURL, want string
|
|
}{
|
|
{name: "empty"},
|
|
{name: "whitespace", profileURL: " "},
|
|
{name: "explicit", profileURL: "https://portal.example.com/account", want: "https://portal.example.com/account"},
|
|
{name: "template", profileURL: "https://portal.example.com/account?sub={{SUB_ID}}", want: "https://portal.example.com/account?sub=profile-sub"},
|
|
} {
|
|
t.Run(config.name, func(t *testing.T) {
|
|
for _, client := range []struct {
|
|
name, userAgent string
|
|
happAutoDetect bool
|
|
}{
|
|
{name: "Happ", userAgent: "Happ/3.22.0 (Android)", happAutoDetect: true},
|
|
{name: "Happ without auto-detection", userAgent: "Happ/3.22.0 (Android)"},
|
|
{name: "standard", userAgent: "v2rayNG/1.8.5", happAutoDetect: true},
|
|
} {
|
|
t.Run(client.name, func(t *testing.T) {
|
|
// All formats must honor the opt-in, independently of Happ customization.
|
|
router := gin.New()
|
|
NewSUBController(router.Group("/"),
|
|
WithSUBJsonEnabled(true), WithSUBClashEnabled(true),
|
|
WithSUBProfileURL(config.profileURL),
|
|
WithSUBProfileMode(service.SubProfileModeCustom),
|
|
WithSUBHappConfig(HappConfig{AutoDetect: client.happAutoDetect}),
|
|
)
|
|
for _, path := range []string{"/sub/profile-sub", "/json/profile-sub", "/clash/profile-sub"} {
|
|
t.Run(path, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Host = "sub.example.com"
|
|
req.Header.Set("User-Agent", client.userAgent)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", resp.Code, resp.Body.String())
|
|
}
|
|
if got := resp.Header().Get("Profile-Web-Page-Url"); got != config.want {
|
|
t.Fatalf("Profile-Web-Page-Url = %q, want %q", got, config.want)
|
|
}
|
|
if config.want == "" {
|
|
if _, present := resp.Header()["Profile-Web-Page-Url"]; present {
|
|
t.Fatal("unconfigured profile header must be omitted")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMetadataForSubRequestUsesStableClientIdentity(t *testing.T) {
|
|
initSubDB(t)
|
|
db := database.GetDB()
|
|
first := model.ClientRecord{
|
|
Email: "john doe@example.com",
|
|
SubID: "sub-123",
|
|
UUID: "abcdef12-3456-7890-abcd-ef1234567890",
|
|
TgID: 42,
|
|
Enable: true,
|
|
}
|
|
second := model.ClientRecord{
|
|
Email: "jane@example.com",
|
|
SubID: "sub-123",
|
|
UUID: "fedcba98-3456-7890-abcd-ef1234567890",
|
|
TgID: 99,
|
|
Enable: true,
|
|
}
|
|
if err := db.Create(&first).Error; err != nil {
|
|
t.Fatalf("seed first client: %v", err)
|
|
}
|
|
if err := db.Create(&second).Error; err != nil {
|
|
t.Fatalf("seed second client: %v", err)
|
|
}
|
|
|
|
a := &SUBController{
|
|
subTitle: "isVPN — {{EMAIL}}",
|
|
subSupportUrl: "https://support.example/?email={{EMAIL}}&tg={{TELEGRAM_ID}}",
|
|
subProfileUrl: "https://profile.example/account/{{ID}}",
|
|
subProfileMode: service.SubProfileModeCustom,
|
|
subAnnounce: "Subscription {{SUB_ID}}",
|
|
}
|
|
metadata := a.metadataForSubRequest(func() *SubService { return &SubService{} }, "sub-123", "https://sub.example/sub-123")
|
|
|
|
if metadata.Title != "isVPN — john doe@example.com" {
|
|
t.Fatalf("Title = %q", metadata.Title)
|
|
}
|
|
if metadata.SupportURL != "https://support.example/?email=john+doe%40example.com&tg=42" {
|
|
t.Fatalf("SupportURL = %q", metadata.SupportURL)
|
|
}
|
|
if metadata.ProfileURL != "https://profile.example/account/abcdef12-3456-7890-abcd-ef1234567890" {
|
|
t.Fatalf("ProfileURL = %q", metadata.ProfileURL)
|
|
}
|
|
if metadata.Announce != "Subscription sub-123" {
|
|
t.Fatalf("Announce = %q", metadata.Announce)
|
|
}
|
|
}
|