mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +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.
125 lines
4.6 KiB
Go
125 lines
4.6 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/sub"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
)
|
|
|
|
// recordingBotServer answers sendMessage and records every text it was asked to send.
|
|
func recordingBotServer(t *testing.T) func() []string {
|
|
t.Helper()
|
|
var mu sync.Mutex
|
|
var texts []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
method := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]
|
|
switch method {
|
|
case "sendMessage", "sendPhoto":
|
|
// A screen is a photo message, so its caption travels as multipart
|
|
// form data; the raw body carries it either way.
|
|
raw, _ := io.ReadAll(r.Body)
|
|
mu.Lock()
|
|
texts = append(texts, string(raw))
|
|
mu.Unlock()
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "result": map[string]any{
|
|
"message_id": 1, "date": 0, "chat": map[string]any{"id": ownerTgID, "type": "private"},
|
|
}})
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
swapTestBot(t, srv.URL)
|
|
return func() []string {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return append([]string(nil), texts...)
|
|
}
|
|
}
|
|
|
|
// With no sub or web domain set, the only host the bot knows is the machine
|
|
// name, which need not resolve; the links must not depend on reaching it.
|
|
func TestIndividualLinksDoNotNeedAResolvableHost(t *testing.T) {
|
|
sent := recordingBotServer(t)
|
|
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
|
service.RegisterSubLinkProvider(sub.NewLinkProvider())
|
|
|
|
const uuid = "11111111-2222-4333-8444-555555555555"
|
|
db := database.GetDB()
|
|
ib := &model.Inbound{
|
|
UserId: 1, Tag: "in-443", Enable: true, Listen: "203.0.113.5", Port: 443,
|
|
Protocol: model.VLESS, Remark: "in",
|
|
Settings: `{"clients":[{"id":"` + uuid + `","email":"` + ownerMail + `","tgId":4242,"subId":"sub-owned","enable":true}],"decryption":"none"}`,
|
|
}
|
|
if err := db.Create(ib).Error; err != nil {
|
|
t.Fatalf("seed inbound: %v", err)
|
|
}
|
|
rec := &model.ClientRecord{Email: ownerMail, SubID: "sub-owned", UUID: uuid, TgID: ownerTgID, Enable: true}
|
|
if err := db.Create(rec).Error; err != nil {
|
|
t.Fatalf("seed client: %v", err)
|
|
}
|
|
if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: ib.Id}).Error; err != nil {
|
|
t.Fatalf("seed client_inbound: %v", err)
|
|
}
|
|
if err := db.Create(&xray.ClientTraffic{InboundId: ib.Id, Email: ownerMail, Enable: true}).Error; err != nil {
|
|
t.Fatalf("seed traffic: %v", err)
|
|
}
|
|
|
|
origHost, origRunning := hostname, isRunning
|
|
t.Cleanup(func() { hostname, isRunning = origHost, origRunning })
|
|
hostname, isRunning = "unresolvable-panel-host.invalid", true
|
|
|
|
tapClientLinks(t, &Tgbot{}, ownerTgID, "client_individual_links "+ownerMail)
|
|
|
|
got := strings.Join(sent(), "\n")
|
|
if !strings.Contains(got, "vless://"+uuid+"@203.0.113.5:443") {
|
|
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)
|
|
}
|
|
}
|