mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
fix: chunk large IN ? queries and fix IPv6 same-origin check
This commit is contained in:
parent
2bb3170ab0
commit
f0d6966724
2 changed files with 119 additions and 25 deletions
|
|
@ -46,7 +46,13 @@ func checkSameOrigin(r *http.Request) bool {
|
||||||
}
|
}
|
||||||
host, _, err := net.SplitHostPort(r.Host)
|
host, _, err := net.SplitHostPort(r.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// IPv6 literal без порта приходит как "[::1]" — net.SplitHostPort
|
||||||
|
// в этом случае ошибается, а url.Hostname() возвращает адрес без
|
||||||
|
// скобок. Снимаем их вручную, чтобы same-origin не отказывал на IPv6.
|
||||||
host = r.Host
|
host = r.Host
|
||||||
|
if len(host) >= 2 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||||
|
host = host[1 : len(host)-1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return strings.EqualFold(u.Hostname(), host)
|
return strings.EqualFold(u.Hostname(), host)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,7 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||||
// Bulk-delete client IPs for every email in this inbound. The previous
|
// Bulk-delete client IPs for every email in this inbound. The previous
|
||||||
// per-client loop fired one DELETE per row — at 7k+ clients that meant
|
// per-client loop fired one DELETE per row — at 7k+ clients that meant
|
||||||
// thousands of synchronous SQL roundtrips and a multi-second freeze.
|
// thousands of synchronous SQL roundtrips and a multi-second freeze.
|
||||||
|
// Chunked to stay under SQLite's bind-variable limit on huge inbounds.
|
||||||
if len(clients) > 0 {
|
if len(clients) > 0 {
|
||||||
emails := make([]string, 0, len(clients))
|
emails := make([]string, 0, len(clients))
|
||||||
for i := range clients {
|
for i := range clients {
|
||||||
|
|
@ -376,8 +377,8 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||||
emails = append(emails, clients[i].Email)
|
emails = append(emails, clients[i].Email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(emails) > 0 {
|
for _, batch := range chunkStrings(uniqueNonEmptyStrings(emails), sqliteMaxVars) {
|
||||||
if err := db.Where("client_email IN ?", emails).
|
if err := db.Where("client_email IN ?", batch).
|
||||||
Delete(model.InboundClientIps{}).Error; err != nil {
|
Delete(model.InboundClientIps{}).Error; err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
@ -1528,9 +1529,14 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
|
||||||
for _, traffic := range traffics {
|
for _, traffic := range traffics {
|
||||||
inbound_ids = append(inbound_ids, traffic.InboundId)
|
inbound_ids = append(inbound_ids, traffic.InboundId)
|
||||||
}
|
}
|
||||||
err = tx.Model(model.Inbound{}).Where("id IN ?", inbound_ids).Find(&inbounds).Error
|
// Chunked to stay under SQLite's bind-variable limit when many inbounds
|
||||||
if err != nil {
|
// are touched in a single tick.
|
||||||
return false, 0, err
|
for _, batch := range chunkInts(inbound_ids, sqliteMaxVars) {
|
||||||
|
var page []*model.Inbound
|
||||||
|
if err = tx.Model(model.Inbound{}).Where("id IN ?", batch).Find(&page).Error; err != nil {
|
||||||
|
return false, 0, err
|
||||||
|
}
|
||||||
|
inbounds = append(inbounds, page...)
|
||||||
}
|
}
|
||||||
for inbound_index := range inbounds {
|
for inbound_index := range inbounds {
|
||||||
settings := map[string]any{}
|
settings := map[string]any{}
|
||||||
|
|
@ -2390,15 +2396,24 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var traffics []*xray.ClientTraffic
|
// Chunked to stay under SQLite's bind-variable limit when a single Telegram
|
||||||
err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error
|
// account owns thousands of clients across inbounds.
|
||||||
if err != nil {
|
uniqEmails := uniqueNonEmptyStrings(emails)
|
||||||
if err == gorm.ErrRecordNotFound {
|
traffics := make([]*xray.ClientTraffic, 0, len(uniqEmails))
|
||||||
logger.Warning("No ClientTraffic records found for emails:", emails)
|
for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) {
|
||||||
return nil, nil
|
var page []*xray.ClientTraffic
|
||||||
|
if err = db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", batch, err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", emails, err)
|
traffics = append(traffics, page...)
|
||||||
return nil, err
|
}
|
||||||
|
if len(traffics) == 0 {
|
||||||
|
logger.Warning("No ClientTraffic records found for emails:", emails)
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate UUID and other client data for each traffic record
|
// Populate UUID and other client data for each traffic record
|
||||||
|
|
@ -2413,18 +2428,86 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
|
||||||
return traffics, nil
|
return traffics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sqliteMaxVars is a safe ceiling for the number of bind parameters in a
|
||||||
|
// single SQL statement. SQLite's SQLITE_MAX_VARIABLE_NUMBER is 999 on builds
|
||||||
|
// before 3.32 and 32766 after; staying under 999 keeps queries portable
|
||||||
|
// across forks/old binaries and also bounds per-query memory on truly large
|
||||||
|
// installs (>32k clients) where even modern SQLite would refuse a single IN.
|
||||||
|
const sqliteMaxVars = 900
|
||||||
|
|
||||||
|
// uniqueNonEmptyStrings returns a deduplicated copy of in with empty strings
|
||||||
|
// removed, preserving the order of first occurrence.
|
||||||
|
func uniqueNonEmptyStrings(in []string) []string {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(in))
|
||||||
|
out := make([]string, 0, len(in))
|
||||||
|
for _, v := range in {
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// chunkStrings splits s into consecutive sub-slices of at most size elements.
|
||||||
|
// Returns nil for an empty input or non-positive size.
|
||||||
|
func chunkStrings(s []string, size int) [][]string {
|
||||||
|
if size <= 0 || len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([][]string, 0, (len(s)+size-1)/size)
|
||||||
|
for i := 0; i < len(s); i += size {
|
||||||
|
end := i + size
|
||||||
|
if end > len(s) {
|
||||||
|
end = len(s)
|
||||||
|
}
|
||||||
|
out = append(out, s[i:end])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// chunkInts splits s into consecutive sub-slices of at most size elements.
|
||||||
|
// Returns nil for an empty input or non-positive size.
|
||||||
|
func chunkInts(s []int, size int) [][]int {
|
||||||
|
if size <= 0 || len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([][]int, 0, (len(s)+size-1)/size)
|
||||||
|
for i := 0; i < len(s); i += size {
|
||||||
|
end := i + size
|
||||||
|
if end > len(s) {
|
||||||
|
end = len(s)
|
||||||
|
}
|
||||||
|
out = append(out, s[i:end])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// GetActiveClientTraffics returns the absolute ClientTraffic rows for the given
|
// GetActiveClientTraffics returns the absolute ClientTraffic rows for the given
|
||||||
// emails in a single batched query. Used by the WebSocket delta path to push
|
// emails. Used by the WebSocket delta path to push per-client absolute
|
||||||
// per-client absolute counters without re-serializing the full inbound list.
|
// counters without re-serializing the full inbound list. The query is chunked
|
||||||
// Empty input or a "record not found" result returns an empty slice.
|
// to stay under SQLite's bind-variable limit on very large active sets.
|
||||||
|
// Empty input returns (nil, nil).
|
||||||
func (s *InboundService) GetActiveClientTraffics(emails []string) ([]*xray.ClientTraffic, error) {
|
func (s *InboundService) GetActiveClientTraffics(emails []string) ([]*xray.ClientTraffic, error) {
|
||||||
if len(emails) == 0 {
|
uniq := uniqueNonEmptyStrings(emails)
|
||||||
|
if len(uniq) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var traffics []*xray.ClientTraffic
|
traffics := make([]*xray.ClientTraffic, 0, len(uniq))
|
||||||
if err := db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error; err != nil {
|
for _, batch := range chunkStrings(uniq, sqliteMaxVars) {
|
||||||
return nil, err
|
var page []*xray.ClientTraffic
|
||||||
|
if err := db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
traffics = append(traffics, page...)
|
||||||
}
|
}
|
||||||
return traffics, nil
|
return traffics, nil
|
||||||
}
|
}
|
||||||
|
|
@ -2824,11 +2907,16 @@ func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
|
||||||
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
|
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
// Step 1: Get ClientTraffic records for emails in the input list
|
// Step 1: Get ClientTraffic records for emails in the input list.
|
||||||
var clients []xray.ClientTraffic
|
// Chunked to stay under SQLite's bind-variable limit on huge inputs.
|
||||||
err := db.Where("email IN ?", emails).Find(&clients).Error
|
uniqEmails := uniqueNonEmptyStrings(emails)
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
clients := make([]xray.ClientTraffic, 0, len(uniqEmails))
|
||||||
return nil, nil, err
|
for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) {
|
||||||
|
var page []xray.ClientTraffic
|
||||||
|
if err := db.Where("email IN ?", batch).Find(&page).Error; err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
clients = append(clients, page...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Sort clients by (Up + Down) descending
|
// Step 2: Sort clients by (Up + Down) descending
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue