mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
2467b9d542
A subscription's size is the product of its clients and inbounds with each one's remark and endpoints, and nothing bounded that product: data adopted from a node could make one unauthenticated request render gigabytes and run the panel out of memory. Each request now carries an 8 MiB budget that the raw, JSON and Clash outputs charge as they materialize a client's links, configs or proxies. Rendering stops once it is spent, with one warning, and anything below the budget is unchanged.
72 lines
2.6 KiB
Go
72 lines
2.6 KiB
Go
package sub
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// budgetRepeats copies of budgetRemark are well past maxRenderedBytes.
|
|
var budgetRemark = strings.Repeat("r", 100_000)
|
|
|
|
const budgetRepeats = 200
|
|
|
|
func renderedSubscriptionSizes(t *testing.T, subId string) map[string]int {
|
|
t.Helper()
|
|
base := NewSubService("")
|
|
links, _, _, _, err := base.GetSubs(subId, "req.example.com")
|
|
if err != nil {
|
|
t.Fatalf("GetSubs: %v", err)
|
|
}
|
|
jsonBody, _, err := NewSubJsonService("", "", "", "", base).GetJson(subId, "req.example.com", false)
|
|
if err != nil {
|
|
t.Fatalf("GetJson: %v", err)
|
|
}
|
|
clashBody, _, err := NewSubClashService(false, "", false, base).GetClash(subId, "req.example.com")
|
|
if err != nil {
|
|
t.Fatalf("GetClash: %v", err)
|
|
}
|
|
return map[string]int{"raw": len(strings.Join(links, "\n")), "json": len(jsonBody), "clash": len(clashBody)}
|
|
}
|
|
|
|
// The final documents indent the booked JSON configs again and list every Clash
|
|
// proxy name a second time in the PROXY group, hence the factor of two.
|
|
func assertBoundedSubscription(t *testing.T, subId string) {
|
|
t.Helper()
|
|
for format, size := range renderedSubscriptionSizes(t, subId) {
|
|
if size < len(budgetRemark) || size > 2*maxRenderedBytes {
|
|
t.Errorf("%s subscription is %d bytes, want %d..%d", format, size, len(budgetRemark), 2*maxRenderedBytes)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clients sharing a subId repeat the remark just like endpoints do, each in a
|
|
// fan-out of its own, so the response as a whole needs the bound too.
|
|
func TestSubscriptionBoundsRenderedSizeAcrossClients(t *testing.T) {
|
|
seedSubDB(t)
|
|
db := database.GetDB()
|
|
ib := seedSubInbound(t, "s1", "many", 4431, 1, `{"network":"tcp","security":"none"}`)
|
|
var clients strings.Builder
|
|
for i := range budgetRepeats {
|
|
uuid, email := fmt.Sprintf("11111111-2222-4333-8444-%012d", i), fmt.Sprintf("c%d@e", i)
|
|
fmt.Fprintf(&clients, `{"id":%q,"email":%q,"subId":"s1","enable":true},`, uuid, email)
|
|
record := &model.ClientRecord{Email: email, SubID: "s1", UUID: uuid, Enable: true}
|
|
if err := db.Create(record).Error; err != nil {
|
|
t.Fatalf("seed client %s: %v", email, err)
|
|
}
|
|
if err := db.Create(&model.ClientInbound{ClientId: record.Id, InboundId: ib.Id}).Error; err != nil {
|
|
t.Fatalf("seed client_inbound %s: %v", email, err)
|
|
}
|
|
}
|
|
if err := db.Model(ib).Updates(map[string]any{
|
|
"remark": budgetRemark,
|
|
"settings": `{"clients":[` + strings.TrimSuffix(clients.String(), ",") + `],"decryption":"none"}`,
|
|
}).Error; err != nil {
|
|
t.Fatalf("update inbound: %v", err)
|
|
}
|
|
assertBoundedSubscription(t, "s1")
|
|
}
|