mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-01 18:56:05 +03:00
fix(sub): resolve a blank subId to no subscription
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.
This commit is contained in:
@@ -36,6 +36,9 @@ type expandedLink struct {
|
||||
// getClientExternalLinksBySubId returns active rows with owner state attached.
|
||||
// Consumers keep inactive owners as metadata but omit their link values.
|
||||
func (s *SubService) getClientExternalLinksBySubId(subId string) ([]externalLinkEntry, error) {
|
||||
if strings.TrimSpace(subId) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
db := database.GetDB()
|
||||
var recs []model.ClientRecord
|
||||
if err := db.Where("sub_id = ?", subId).Find(&recs).Error; err != nil {
|
||||
|
||||
@@ -113,3 +113,25 @@ func TestInactiveExternalOnlySubRemainsKnownWithoutExposingLinks(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,8 +737,12 @@ func subscriptionExpiryFromClient(nowMs, expiryTime int64) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// getInboundsBySubId loads the subscriber's inbounds; hosts are read per request, never cached.
|
||||
// getInboundsBySubId loads the subscriber's inbounds, hosts read per request; a blank subId
|
||||
// would match every client stored without one, so it names no subscription.
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
if strings.TrimSpace(subId) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
inbounds, err := s.loadInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -232,6 +232,11 @@ func (t *Tgbot) clientSubLinks(email, subURL string) ([]string, error) {
|
||||
if err != nil || client == nil {
|
||||
return nil, errors.New("client not found")
|
||||
}
|
||||
// A client stored without a subId has no subscription to look up; build
|
||||
// its own links by email, as the panel's client page does.
|
||||
if strings.TrimSpace(client.SubID) == "" {
|
||||
return t.inboundService.GetAllClientLinks(u.Hostname(), email)
|
||||
}
|
||||
return t.inboundService.GetSubLinks(u.Hostname(), client.SubID)
|
||||
}
|
||||
|
||||
|
||||
@@ -87,3 +87,38 @@ func TestIndividualLinksDoNotNeedAResolvableHost(t *testing.T) {
|
||||
t.Fatalf("bot sent %q, want the client's vless link", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A client stored without a subId has no subscription; the blank value must not
|
||||
// be looked up as one, or it selects every other client stored without a subId.
|
||||
func TestIndividualLinksOfBlankSubIdClientExposeNoOtherClient(t *testing.T) {
|
||||
sent := recordingBotServer(t)
|
||||
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
||||
service.RegisterSubLinkProvider(sub.NewLinkProvider())
|
||||
|
||||
const ownerUUID = "11111111-2222-4333-8444-555555555555"
|
||||
const otherUUID = "99999999-2222-4333-8444-555555555555"
|
||||
inboundSvc := service.InboundService{}
|
||||
_, _, err := inboundSvc.AddInbound(&model.Inbound{
|
||||
UserId: 1, Tag: "in-443", Enable: true, Listen: "203.0.113.5", Port: 443,
|
||||
Protocol: model.VLESS, Remark: "in", StreamSettings: `{"network":"tcp","security":"none"}`,
|
||||
Settings: `{"clients":[{"id":"` + ownerUUID + `","email":"` + ownerMail + `","tgId":4242,"enable":true},` +
|
||||
`{"id":"` + otherUUID + `","email":"other@x","enable":true}],"decryption":"none"}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("AddInbound: %v", err)
|
||||
}
|
||||
|
||||
origRunning := isRunning
|
||||
t.Cleanup(func() { isRunning = origRunning })
|
||||
isRunning = true
|
||||
|
||||
tapClientLinks(t, &Tgbot{}, ownerTgID, "client_individual_links "+ownerMail)
|
||||
|
||||
got := strings.Join(sent(), "\n")
|
||||
if !strings.Contains(got, "vless://"+ownerUUID+"@203.0.113.5:443") {
|
||||
t.Errorf("bot sent %q, want the client's own vless link", got)
|
||||
}
|
||||
if strings.Contains(got, otherUUID) {
|
||||
t.Errorf("bot sent another client's credentials to %s: %q", ownerMail, got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user