mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
1c748adadf
A subId is the only selector of a subscription's inbounds and external links, so a blank one matched every client stored without a subId, as clients embedded through inbounds/add or inbounds/import are. The Telegram bot authorised a client by email and then looked its links up by that blank subId, handing a bound user the working links of every other subId-less client. A blank subId now resolves to nothing in the subscription service, and the bot builds a subId-less client's own links by email, as the panel's client page does.
138 lines
5.1 KiB
Go
138 lines
5.1 KiB
Go
package sub
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/xray"
|
|
)
|
|
|
|
func seedInactiveExternalOnlySub(t *testing.T, subID, email string, enabled bool, expiry int64) {
|
|
t.Helper()
|
|
db := database.GetDB()
|
|
rec := &model.ClientRecord{Email: email, SubID: subID, UUID: subID + "-uuid", Enable: true, ExpiryTime: expiry}
|
|
if err := db.Create(rec).Error; err != nil {
|
|
t.Fatalf("seed client: %v", err)
|
|
}
|
|
if !enabled {
|
|
if err := db.Model(rec).Update("enable", false).Error; err != nil {
|
|
t.Fatalf("disable client: %v", err)
|
|
}
|
|
}
|
|
if err := db.Create(&xray.ClientTraffic{Email: email, Up: 11, Down: 22, Total: 1024, ExpiryTime: expiry}).Error; err != nil {
|
|
t.Fatalf("seed traffic: %v", err)
|
|
}
|
|
link := "vless://11111111-1111-1111-1111-111111111111@example.com:443?type=tcp&security=reality&pbk=abc&sid=12&fp=chrome#external"
|
|
if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: link, SortIndex: 1}).Error; err != nil {
|
|
t.Fatalf("seed external link: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInactiveExternalOnlySubRemainsKnownWithoutExposingLinks(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
states := []struct {
|
|
name string
|
|
enabled bool
|
|
expiry int64
|
|
}{
|
|
{name: "disabled", enabled: false, expiry: time.Now().Add(time.Hour).UnixMilli()},
|
|
{name: "expired", enabled: true, expiry: time.Now().Add(-time.Hour).UnixMilli()},
|
|
}
|
|
|
|
for _, state := range states {
|
|
t.Run(state.name, func(t *testing.T) {
|
|
initSubDB(t)
|
|
subID := "external-" + state.name
|
|
email := state.name + "@example.com"
|
|
seedInactiveExternalOnlySub(t, subID, email, state.enabled, state.expiry)
|
|
|
|
oldDistFS := distFS
|
|
distFS = testDistFS
|
|
t.Cleanup(func() { distFS = oldDistFS })
|
|
|
|
router := gin.New()
|
|
NewSUBController(
|
|
router.Group("/"),
|
|
WithSUBJsonEnabled(true),
|
|
WithSUBClashEnabled(true),
|
|
WithSUBEncryption(false),
|
|
)
|
|
|
|
wantHeader := fmt.Sprintf("upload=11; download=22; total=1024; expire=%d", state.expiry/1000)
|
|
for _, path := range []string{"/sub/" + subID, "/json/" + subID + "?view=raw", "/clash/" + subID + "?view=raw"} {
|
|
t.Run(path, func(t *testing.T) {
|
|
if err := database.GetDB().Model(&xray.ClientTraffic{}).Where("email = ?", email).Update("last_sub_fetch", 0).Error; err != nil {
|
|
t.Fatalf("reset last_sub_fetch: %v", err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Host = "sub.example.com"
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
}
|
|
if w.Body.Len() != 0 {
|
|
t.Fatalf("inactive external link leaked in body: %s", w.Body.String())
|
|
}
|
|
if got := w.Header().Get("Subscription-Userinfo"); got != wantHeader {
|
|
t.Fatalf("Subscription-Userinfo = %q, want %q", got, wantHeader)
|
|
}
|
|
var traffic xray.ClientTraffic
|
|
if err := database.GetDB().Where("email = ?", email).First(&traffic).Error; err != nil {
|
|
t.Fatalf("load traffic: %v", err)
|
|
}
|
|
if traffic.LastSubFetch == 0 {
|
|
t.Fatal("successful empty response did not update last_sub_fetch")
|
|
}
|
|
})
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/sub/"+subID, nil)
|
|
req.Host = "sub.example.com"
|
|
req.Header.Set("Accept", "text/html")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("HTML status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
}
|
|
if strings.Contains(w.Body.String(), "11111111-1111-1111-1111-111111111111") {
|
|
t.Fatalf("HTML page exposed inactive external link: %s", w.Body.String())
|
|
}
|
|
if !strings.Contains(w.Body.String(), `"links":[]`) {
|
|
t.Fatalf("HTML page did not render an empty links list: %s", w.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Clients stored without a subId share no subscription; a blank key must not
|
|
// select all of them. The routes never pass one, in-process callers can.
|
|
func TestBlankSubIdResolvesToNothing(t *testing.T) {
|
|
for _, subId := range []string{"", " "} {
|
|
seedSubDB(t)
|
|
seedSubInbound(t, subId, "one", 4431, 1, `{"network":"tcp","security":"none"}`)
|
|
seedSubInbound(t, subId, "two", 4432, 2, `{"network":"tcp","security":"none"}`)
|
|
seedInactiveExternalOnlySub(t, subId, "ext@e", true, time.Now().Add(time.Hour).UnixMilli())
|
|
|
|
links, emails, _, _, err := NewSubService("").GetSubs(subId, "req.example.com")
|
|
if err != nil || len(links) != 0 || len(emails) != 0 {
|
|
t.Errorf("GetSubs(%q) = %v, %v, %v; want nothing", subId, links, emails, err)
|
|
}
|
|
if out, _, err := NewSubJsonService("", "", "", "", NewSubService("")).GetJson(subId, "req.example.com", false); err != nil || out != "" {
|
|
t.Errorf("GetJson(%q) = %q, %v; want nothing", subId, out, err)
|
|
}
|
|
if out, _, err := NewSubClashService(false, "", false, NewSubService("")).GetClash(subId, "req.example.com"); err != nil || out != "" {
|
|
t.Errorf("GetClash(%q) = %q, %v; want nothing", subId, out, err)
|
|
}
|
|
}
|
|
}
|