chore: apply modernize analyzer fixes across codebase

Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
This commit is contained in:
MHSanaei 2026-05-18 12:22:02 +02:00
parent 317c7cb4f1
commit 1ef494bcda
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
24 changed files with 69 additions and 94 deletions

View file

@ -69,11 +69,11 @@ func isIgnorableDuplicateColumnErr(err error, mdl any) bool {
if !strings.Contains(errMsg, dupPrefix) { if !strings.Contains(errMsg, dupPrefix) {
return false return false
} }
idx := strings.Index(errMsg, dupPrefix) _, after, ok := strings.Cut(errMsg, dupPrefix)
if idx < 0 { if !ok {
return false return false
} }
col := strings.TrimSpace(errMsg[idx+len(dupPrefix):]) col := strings.TrimSpace(after)
col = strings.Trim(col, "`\"[]") col = strings.Trim(col, "`\"[]")
if col == "" { if col == "" {
return false return false

4
go.mod
View file

@ -12,7 +12,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v1.8.0 github.com/mymmrac/telego v1.9.0
github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
@ -25,7 +25,7 @@ require (
golang.org/x/crypto v0.51.0 golang.org/x/crypto v0.51.0
golang.org/x/sys v0.44.0 golang.org/x/sys v0.44.0
golang.org/x/text v0.37.0 golang.org/x/text v0.37.0
google.golang.org/grpc v1.81.0 google.golang.org/grpc v1.81.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1

8
go.sum
View file

@ -130,8 +130,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow= github.com/mymmrac/telego v1.9.0 h1:ZUJxZaPx/1IgRvVb5lXnUB8FgW5rNYfRe6Q2EJ4OJ+Y=
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM= github.com/mymmrac/telego v1.9.0/go.mod h1:tVEB7OqiOPx8elRk9+ETkwiDQrUhWSB2XmAKIY9KmWY=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@ -258,8 +258,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -207,9 +207,9 @@ func (s *Server) initRouter() (*gin.Engine, error) {
path := c.Request.URL.Path path := c.Request.URL.Path
pathPrefix := strings.TrimRight(LinksPath, "/") + "/" pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") { if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
assetsIndex := strings.Index(path, "/assets/") _, after, ok := strings.Cut(path, "/assets/")
if assetsIndex != -1 { if ok {
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/" assetPath := after // +8 to skip "/assets/"
if assetPath != "" { if assetPath != "" {
c.FileFromFS(assetPath, assetsFS) c.FileFromFS(assetPath, assetsFS)
c.Abort() c.Abort()

View file

@ -2,6 +2,7 @@ package sub
import ( import (
"fmt" "fmt"
"maps"
"strings" "strings"
"github.com/goccy/go-json" "github.com/goccy/go-json"
@ -471,8 +472,6 @@ func cloneMap(src map[string]any) map[string]any {
return nil return nil
} }
dst := make(map[string]any, len(src)) dst := make(map[string]any, len(src))
for k, v := range src { maps.Copy(dst, src)
dst[k] = v
}
return dst return dst
} }

View file

@ -3,6 +3,7 @@ package ldaputil
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"slices"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
) )
@ -82,13 +83,7 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
continue continue
} }
val := e.GetAttributeValue(cfg.FlagField) val := e.GetAttributeValue(cfg.FlagField)
enabled := false enabled := slices.Contains(cfg.TruthyVals, val)
for _, t := range cfg.TruthyVals {
if val == t {
enabled = true
break
}
}
if cfg.Invert { if cfg.Invert {
enabled = !enabled enabled = !enabled
} }

View file

@ -32,7 +32,7 @@ func TestSeq_NotConstant(t *testing.T) {
func TestNum_InRange(t *testing.T) { func TestNum_InRange(t *testing.T) {
for _, upper := range []int{1, 2, 10, 1000} { for _, upper := range []int{1, 2, 10, 1000} {
for i := 0; i < 200; i++ { for range 200 {
v := Num(upper) v := Num(upper)
if v < 0 || v >= upper { if v < 0 || v >= upper {
t.Fatalf("Num(%d) returned %d, out of [0, %d)", upper, v, upper) t.Fatalf("Num(%d) returned %d, out of [0, %d)", upper, v, upper)

View file

@ -1,5 +1,4 @@
//go:build darwin //go:build darwin
// +build darwin
package sys package sys

View file

@ -1,5 +1,4 @@
//go:build linux //go:build linux
// +build linux
package sys package sys

View file

@ -1,5 +1,4 @@
//go:build windows //go:build windows
// +build windows
package sys package sys

View file

@ -32,8 +32,8 @@ func NewAPIController(g *gin.RouterGroup, customGeo *service.CustomGeoService) *
func (a *APIController) checkAPIAuth(c *gin.Context) { func (a *APIController) checkAPIAuth(c *gin.Context) {
auth := c.GetHeader("Authorization") auth := c.GetHeader("Authorization")
if strings.HasPrefix(auth, "Bearer ") { if after, ok := strings.CutPrefix(auth, "Bearer "); ok {
tok := strings.TrimPrefix(auth, "Bearer ") tok := after
if a.apiTokenService.Match(tok) { if a.apiTokenService.Match(tok) {
if u, err := a.userService.GetFirstUser(); err == nil { if u, err := a.userService.GetFirstUser(); err == nil {
session.SetAPIAuthUser(c, u) session.SetAPIAuthUser(c, u)

View file

@ -27,7 +27,7 @@ func getRemoteIp(c *gin.Context) string {
} }
if xff := c.GetHeader("X-Forwarded-For"); xff != "" { if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
for _, part := range strings.Split(xff, ",") { for part := range strings.SplitSeq(xff, ",") {
if ip, ok := extractTrustedIP(part); ok { if ip, ok := extractTrustedIP(part); ok {
return ip return ip
} }
@ -50,7 +50,7 @@ func isTrustedProxy(ip string) bool {
} }
trusted := trustedProxyCIDRs() trusted := trustedProxyCIDRs()
for _, value := range strings.Split(trusted, ",") { for value := range strings.SplitSeq(trusted, ",") {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
continue continue

View file

@ -195,7 +195,7 @@ func (s *AllSetting) CheckValid() error {
s.SubClashPath += "/" s.SubClashPath += "/"
} }
for _, cidr := range strings.Split(s.TrustedProxyCIDRs, ",") { for cidr := range strings.SplitSeq(s.TrustedProxyCIDRs, ",") {
cidr = strings.TrimSpace(cidr) cidr = strings.TrimSpace(cidr)
if cidr == "" { if cidr == "" {
continue continue

View file

@ -42,28 +42,24 @@ func TestAtomicBool_ConcurrentSettersExactlyOneTakeWins(t *testing.T) {
const readers = 20 const readers = 20
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < setters; i++ { for range setters {
wg.Add(1) wg.Go(func() {
go func() {
defer wg.Done()
a.set() a.set()
}() })
} }
wg.Wait() wg.Wait()
trueCount := 0 trueCount := 0
var rwg sync.WaitGroup var rwg sync.WaitGroup
var mu sync.Mutex var mu sync.Mutex
for i := 0; i < readers; i++ { for range readers {
rwg.Add(1) rwg.Go(func() {
go func() {
defer rwg.Done()
if a.takeAndReset() { if a.takeAndReset() {
mu.Lock() mu.Lock()
trueCount++ trueCount++
mu.Unlock() mu.Unlock()
} }
}() })
} }
rwg.Wait() rwg.Wait()

View file

@ -2526,10 +2526,7 @@ func chunkStrings(s []string, size int) [][]string {
} }
out := make([][]string, 0, (len(s)+size-1)/size) out := make([][]string, 0, (len(s)+size-1)/size)
for i := 0; i < len(s); i += size { for i := 0; i < len(s); i += size {
end := i + size end := min(i+size, len(s))
if end > len(s) {
end = len(s)
}
out = append(out, s[i:end]) out = append(out, s[i:end])
} }
return out return out
@ -2543,10 +2540,7 @@ func chunkInts(s []int, size int) [][]int {
} }
out := make([][]int, 0, (len(s)+size-1)/size) out := make([][]int, 0, (len(s)+size-1)/size)
for i := 0; i < len(s); i += size { for i := 0; i < len(s); i += size {
end := i + size end := min(i+size, len(s))
if end > len(s) {
end = len(s)
}
out = append(out, s[i:end]) out = append(out, s[i:end])
} }
return out return out

View file

@ -213,7 +213,7 @@ func compareVersionStrings(a string, b string) (int, bool) {
if !okA || !okB { if !okA || !okB {
return 0, false return 0, false
} }
for i := 0; i < len(aParts); i++ { for i := range len(aParts) {
if aParts[i] > bParts[i] { if aParts[i] > bParts[i] {
return 1, true return 1, true
} }

View file

@ -72,7 +72,7 @@ func inboundTransports(protocol model.Protocol, streamSettings, settings string)
// "udp", or "tcp,udp". if it's set, it wins outright. // "udp", or "tcp,udp". if it's set, it wins outright.
if n, ok := st["network"].(string); ok && n != "" { if n, ok := st["network"].(string); ok && n != "" {
bits = 0 bits = 0
for _, part := range strings.Split(n, ",") { for part := range strings.SplitSeq(n, ",") {
switch strings.TrimSpace(part) { switch strings.TrimSpace(part) {
case "tcp": case "tcp":
bits |= transportTCP bits |= transportTCP

View file

@ -56,7 +56,8 @@ func seedInboundConflictNode(t *testing.T, tag, listen string, port int, protoco
} }
} }
func intPtr(v int) *int { return &v } //go:fix inline
func intPtr(v int) *int { return new(v) }
func TestInboundTransports(t *testing.T) { func TestInboundTransports(t *testing.T) {
cases := []struct { cases := []struct {
@ -360,7 +361,7 @@ func TestGenerateInboundTag_SpecificListenSameDisambiguation(t *testing.T) {
func TestCheckPortConflict_NodeScope(t *testing.T) { func TestCheckPortConflict_NodeScope(t *testing.T) {
setupConflictDB(t) setupConflictDB(t)
seedInboundConflictNode(t, "local-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, nil) seedInboundConflictNode(t, "local-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, nil)
seedInboundConflictNode(t, "node1-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, intPtr(1)) seedInboundConflictNode(t, "node1-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, new(1))
svc := &InboundService{} svc := &InboundService{}
@ -370,8 +371,8 @@ func TestCheckPortConflict_NodeScope(t *testing.T) {
want bool want bool
}{ }{
{"new local same port + tcp clashes with local", nil, true}, {"new local same port + tcp clashes with local", nil, true},
{"new remote on different node from local is fine", intPtr(2), false}, {"new remote on different node from local is fine", new(2), false},
{"new remote on existing node 1 clashes", intPtr(1), true}, {"new remote on existing node 1 clashes", new(1), true},
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {

View file

@ -227,7 +227,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
parsedAdminIds := make([]int64, 0) parsedAdminIds := make([]int64, 0)
// Parse admin IDs from comma-separated string // Parse admin IDs from comma-separated string
if tgBotID != "" { if tgBotID != "" {
for _, adminID := range strings.Split(tgBotID, ",") { for adminID := range strings.SplitSeq(tgBotID, ",") {
id, err := strconv.ParseInt(adminID, 10, 64) id, err := strconv.ParseInt(adminID, 10, 64)
if err != nil { if err != nil {
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
@ -2051,12 +2051,7 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
// checkAdmin checks if the given Telegram ID is an admin. // checkAdmin checks if the given Telegram ID is an admin.
func checkAdmin(tgId int64) bool { func checkAdmin(tgId int64) bool {
for _, adminId := range adminIds { return slices.Contains(adminIds, tgId)
if adminId == tgId {
return true
}
}
return false
} }
// SendAnswer sends a response message with an inline keyboard to the specified chat. // SendAnswer sends a response message with an inline keyboard to the specified chat.
@ -2373,17 +2368,18 @@ func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
// Send in chunks to respect message length; use monospace formatting // Send in chunks to respect message length; use monospace formatting
const maxPerMessage = 50 const maxPerMessage = 50
for i := 0; i < len(cleaned); i += maxPerMessage { for i := 0; i < len(cleaned); i += maxPerMessage {
j := i + maxPerMessage j := min(i+maxPerMessage, len(cleaned))
if j > len(cleaned) {
j = len(cleaned)
}
chunk := cleaned[i:j] chunk := cleaned[i:j]
msg := t.I18nBot("subscription.individualLinks") + ":\r\n" var msg strings.Builder
msg.WriteString(t.I18nBot("subscription.individualLinks"))
msg.WriteString(":\r\n")
for _, link := range chunk { for _, link := range chunk {
// wrap each link in <code> // wrap each link in <code>
msg += "<code>" + link + "</code>\r\n" msg.WriteString("<code>")
msg.WriteString(link)
msg.WriteString("</code>\r\n")
} }
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg.String())
} }
} }
@ -3439,7 +3435,8 @@ func (t *Tgbot) notifyExhausted() {
var exhaustedClients []xray.ClientTraffic var exhaustedClients []xray.ClientTraffic
traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID) traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID)
if err == nil && len(traffics) > 0 { if err == nil && len(traffics) > 0 {
output := t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")) var output strings.Builder
output.WriteString(t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")))
for _, traffic := range traffics { for _, traffic := range traffics {
if traffic.Enable { if traffic.Enable {
if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) || if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) ||
@ -3451,21 +3448,23 @@ func (t *Tgbot) notifyExhausted() {
} }
} }
if len(exhaustedClients) > 0 { if len(exhaustedClients) > 0 {
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) output.WriteString(t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))))
if len(disabledClients) > 0 { if len(disabledClients) > 0 {
output += t.I18nBot("tgbot.clients") + ":\r\n" output.WriteString(t.I18nBot("tgbot.clients"))
output.WriteString(":\r\n")
for _, traffic := range disabledClients { for _, traffic := range disabledClients {
output += " " + traffic.Email output.WriteString(" ")
output.WriteString(traffic.Email)
} }
output += "\r\n" output.WriteString("\r\n")
} }
output += "\r\n" output.WriteString("\r\n")
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients))) output.WriteString(t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients))))
for _, traffic := range exhaustedClients { for _, traffic := range exhaustedClients {
output += t.clientInfoMsg(&traffic, true, false, false, true, true, false) output.WriteString(t.clientInfoMsg(&traffic, true, false, false, true, true, false))
output += "\r\n" output.WriteString("\r\n")
} }
t.SendMsgToTgbot(chatID, output) t.SendMsgToTgbot(chatID, output.String())
} }
chatIDsDone = append(chatIDsDone, chatID) chatIDsDone = append(chatIDsDone, chatID)
} }
@ -3480,12 +3479,7 @@ func (t *Tgbot) notifyExhausted() {
// int64Contains checks if an int64 slice contains a specific item. // int64Contains checks if an int64 slice contains a specific item.
func int64Contains(slice []int64, item int64) bool { func int64Contains(slice []int64, item int64) bool {
for _, s := range slice { return slices.Contains(slice, item)
if s == item {
return true
}
}
return false
} }
// onlineClients retrieves and sends information about online clients. // onlineClients retrieves and sends information about online clients.

View file

@ -6,7 +6,7 @@ import (
) )
func TestLoginAttemptDoesNotCarryPassword(t *testing.T) { func TestLoginAttemptDoesNotCarryPassword(t *testing.T) {
typ := reflect.TypeOf(LoginAttempt{}) typ := reflect.TypeFor[LoginAttempt]()
if _, ok := typ.FieldByName("Password"); ok { if _, ok := typ.FieldByName("Password"); ok {
t.Fatal("LoginAttempt must not carry attempted passwords") t.Fatal("LoginAttempt must not carry attempted passwords")
} }

View file

@ -3,6 +3,7 @@ package service
import ( import (
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"slices"
"github.com/mhsanaei/3x-ui/v3/util/common" "github.com/mhsanaei/3x-ui/v3/util/common"
"github.com/mhsanaei/3x-ui/v3/xray" "github.com/mhsanaei/3x-ui/v3/xray"
@ -55,7 +56,7 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
// If `raw` does not look like a wrapper, it is returned unchanged. // If `raw` does not look like a wrapper, it is returned unchanged.
func UnwrapXrayTemplateConfig(raw string) string { func UnwrapXrayTemplateConfig(raw string) string {
const maxDepth = 8 // defensive cap against pathological multi-nest values const maxDepth = 8 // defensive cap against pathological multi-nest values
for i := 0; i < maxDepth; i++ { for range maxDepth {
var top map[string]json.RawMessage var top map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &top); err != nil { if err := json.Unmarshal([]byte(raw), &top); err != nil {
return raw return raw
@ -190,11 +191,9 @@ func findApiRule(rules []map[string]any) int {
} }
} }
case []string: case []string:
for _, s := range tags { if slices.Contains(tags, "api") {
if s == "api" {
return i return i
} }
}
case string: case string:
if tags == "api" { if tags == "api" {
return i return i

View file

@ -65,7 +65,7 @@ func TestUnwrapXrayTemplateConfig(t *testing.T) {
// non-wrapped, and confirm we end up at some valid JSON (we // non-wrapped, and confirm we end up at some valid JSON (we
// don't loop forever and we don't blow the stack). // don't loop forever and we don't blow the stack).
s := real s := real
for i := 0; i < 16; i++ { for range 16 {
s = `{"xraySetting":` + s + `}` s = `{"xraySetting":` + s + `}`
} }
got := UnwrapXrayTemplateConfig(s) got := UnwrapXrayTemplateConfig(s)

View file

@ -110,7 +110,7 @@ func (h *Hub) shouldThrottle(msgType MessageType) bool {
// panic doesn't permanently kill real-time updates for commercial deployments. // panic doesn't permanently kill real-time updates for commercial deployments.
// After the cap, the hub stays down and the frontend falls back to REST polling. // After the cap, the hub stays down and the frontend falls back to REST polling.
func (h *Hub) Run() { func (h *Hub) Run() {
for attempt := 0; attempt < hubRestartAttempts; attempt++ { for attempt := range hubRestartAttempts {
stopped := h.runOnce() stopped := h.runOnce()
if stopped { if stopped {
return return

View file

@ -231,7 +231,7 @@ func TestHub_ConcurrentRegisterUnregister(t *testing.T) {
const n = 50 const n = 50
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < n; i++ { for i := range n {
wg.Add(1) wg.Add(1)
go func(idx int) { go func(idx int) {
defer wg.Done() defer wg.Done()