mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-16 16:20:59 +00:00
43bc915397
* fix(sub): show copy-only page for browser subscription visits Browser navigation to /sub previously rendered the normal subscription page, which exposed subscription material in page data or raw base64 depending on request headers. Keep VPN clients on the raw subscription body, but classify browser document requests and return a neutral static copy-only HTML page with no embedded share links or page data. This preserves the C1 LimitIP parser fix in the same master candidate while avoiding a DE rollback of the browser subscription UX. * fix(sub): keep the themed page for an explicit html request Only implicit browser navigation is downgraded to the copy-only page. An operator who appends html=1 or view=html already holds the URL, so the themed subscription page keeps rendering for them and serveSubPage stays in use. * fix(sub): keep browser pages copy-only
134 lines
4.2 KiB
Go
134 lines
4.2 KiB
Go
package sub
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
func TestIsBrowserSubscriptionRequest(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
tests := []struct {
|
|
name string
|
|
accept string
|
|
ua string
|
|
dest string
|
|
mode string
|
|
query string
|
|
want bool
|
|
}{
|
|
{name: "explicit html query is not implicit navigation", query: "?html=1", want: false},
|
|
{name: "html accept", accept: "text/html,application/xhtml+xml", want: true},
|
|
{name: "browser navigation with wildcard accept", accept: "*/*", ua: "Mozilla/5.0 Safari/605.1.15", dest: "document", mode: "navigate", want: true},
|
|
{name: "browser ua fallback", accept: "*/*", ua: "Mozilla/5.0 Chrome/126.0.0.0", want: true},
|
|
{name: "vpn client wildcard", accept: "*/*", ua: "Incy/3.3.0", want: false},
|
|
{name: "vpn client with mozilla token", accept: "*/*", ua: "Mozilla/5.0 Incy/3.3.0", want: false},
|
|
{name: "plain client", accept: "*/*", ua: "Go-http-client/2.0", want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
req := httptest.NewRequest(http.MethodGet, "/sub/abc"+tt.query, nil)
|
|
if tt.accept != "" {
|
|
req.Header.Set("Accept", tt.accept)
|
|
}
|
|
if tt.ua != "" {
|
|
req.Header.Set("User-Agent", tt.ua)
|
|
}
|
|
if tt.dest != "" {
|
|
req.Header.Set("Sec-Fetch-Dest", tt.dest)
|
|
}
|
|
if tt.mode != "" {
|
|
req.Header.Set("Sec-Fetch-Mode", tt.mode)
|
|
}
|
|
c.Request = req
|
|
|
|
if got := (&SUBController{}).isBrowserSubscriptionRequest(c); got != tt.want {
|
|
t.Fatalf("isBrowserSubscriptionRequest() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBrowserClassificationHonorsConfiguredFormatMatchers(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
new func() *SUBController
|
|
}{
|
|
{"clash", func() *SUBController {
|
|
return &SUBController{subClashAutoDetect: true, clashEnabled: true, clashUserAgent: regexp.MustCompile(`Custom-Client`)}
|
|
}},
|
|
{"json", func() *SUBController {
|
|
return &SUBController{jsonAutoDetect: true, jsonEnabled: true, jsonUserAgent: regexp.MustCompile(`Custom-Client`)}
|
|
}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/sub/abc", nil)
|
|
c.Request.Header.Set("User-Agent", "Mozilla/5.0 Custom-Client/1.0")
|
|
if tc.new().isBrowserSubscriptionRequest(c) {
|
|
t.Fatal("configured subscription client was classified as a browser")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionCopyPageUsesRequestLocale(t *testing.T) {
|
|
bundle := i18n.NewBundle(language.English)
|
|
for id, text := range map[string]string{
|
|
"subCopyPageTitle": "Titre localisé",
|
|
"subCopyPageHeading": "En-tête localisé",
|
|
"subCopyPageInstructions": "Instructions localisées",
|
|
} {
|
|
bundle.AddMessages(language.French, &i18n.Message{ID: id, Other: text})
|
|
}
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/sub/abc", nil)
|
|
c.Request.Header.Set("Accept-Language", "fr-FR")
|
|
c.Set("localizer", i18n.NewLocalizer(bundle, "fr-FR"))
|
|
|
|
(&SUBController{}).serveSubscriptionCopyPage(c)
|
|
if body := w.Body.String(); !strings.Contains(body, `<html lang="fr-FR">`) ||
|
|
!strings.Contains(body, "Titre localisé") || !strings.Contains(body, "Instructions localisées") {
|
|
t.Fatalf("copy page was not localized from the request: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestExplicitSubPageRequest(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
want bool
|
|
}{
|
|
{name: "html=1", query: "?html=1", want: true},
|
|
{name: "view=html", query: "?view=HTML", want: true},
|
|
{name: "no query", query: "", want: false},
|
|
{name: "unrelated query", query: "?format=info", want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/sub/abc"+tt.query, nil)
|
|
|
|
if got := explicitSubPageRequest(c); got != tt.want {
|
|
t.Fatalf("explicitSubPageRequest() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|