mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(clients): probe inbound settings with a short literal pattern
Deleting a client by an email no record holds fell back to a settings LIKE '%email%' over every inbound with the raw route parameter, which costs SQLite the settings length times the pattern length while it holds the database; a long near-miss email froze the panel for minutes, and wildcards were not escaped. The pre-filter now uses the first 64 bytes of the email, escaped; the exact client match after it still decides.
This commit is contained in:
@@ -3,7 +3,9 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -197,3 +199,29 @@ func TestRolledBackCreateKeepsTombstone(t *testing.T) {
|
||||
t.Fatal("a create that rolled back withdrew the delete tombstone, so a stale node snapshot can resurrect the deleted client")
|
||||
}
|
||||
}
|
||||
|
||||
// The email reaches a LIKE pre-filter: a long near-miss costs SQLite text times pattern
|
||||
// length while holding the database, and wildcards must not match either.
|
||||
func TestFindInboundIdsByClientEmailTreatsTheEmailLiterally(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
settings := `{"clients":[{"email":"real@x"}],"pad":"` + strings.Repeat("a", 1<<20) + `"}`
|
||||
if err := database.GetDB().Create(&model.Inbound{UserId: 1, Tag: "big", Port: 41601, Protocol: model.VLESS, Settings: settings}).Error; err != nil {
|
||||
t.Fatalf("seed inbound: %v", err)
|
||||
}
|
||||
done := make(chan []int, 1)
|
||||
go func() {
|
||||
ids, _ := (&ClientService{}).findInboundIdsByClientEmail(strings.Repeat("a", 10000) + "b")
|
||||
done <- ids
|
||||
}()
|
||||
select {
|
||||
case ids := <-done:
|
||||
if len(ids) != 0 {
|
||||
t.Fatalf("a wildcard email matched inbounds %v", ids)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the email lookup is still running after 5 s")
|
||||
}
|
||||
if ids, err := (&ClientService{}).findInboundIdsByClientEmail("real@x"); err != nil || len(ids) != 1 {
|
||||
t.Fatalf("lookup of a real email = %v, %v", ids, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
@@ -247,14 +248,26 @@ func (s *ClientService) HasPendingNode(inboundSvc *InboundService, email string)
|
||||
return inboundSvc.AnyNodePending(ids)
|
||||
}
|
||||
|
||||
const settingsProbeBytes = 64
|
||||
|
||||
// findInboundIdsByClientEmail returns every inbound whose settings.clients[]
|
||||
// JSON contains an entry with the given email. Driver-portable (no JSON
|
||||
// operators) by parsing in Go — fine for the rare fallback path.
|
||||
func (s *ClientService) findInboundIdsByClientEmail(email string) ([]int, error) {
|
||||
// A literal, bounded probe: the email comes from a request, and LIKE costs the settings
|
||||
// length times the pattern length; the exact match below decides.
|
||||
probe := email
|
||||
if len(probe) > settingsProbeBytes {
|
||||
cut := settingsProbeBytes
|
||||
for cut > 0 && !utf8.RuneStart(probe[cut]) {
|
||||
cut--
|
||||
}
|
||||
probe = probe[:cut]
|
||||
}
|
||||
var inbounds []model.Inbound
|
||||
if err := database.GetDB().
|
||||
Select("id, settings").
|
||||
Where("settings LIKE ?", "%"+email+"%").
|
||||
Where("settings LIKE ? ESCAPE '\\'", "%"+escapeLikeLiteral(probe)+"%").
|
||||
Find(&inbounds).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user