style: adopt golangci-lint v2 and resolve all findings

Add .golangci.yml (v2): the standard linters plus bodyclose, errorlint, noctx, misspell, rowserrcheck, sqlclosecheck, unconvert, usestdlibvars, with gofumpt + goimports formatters. Enable the std-error-handling exclusion preset for idiomatic Close/Remove/Setenv ignores; scope-exclude SA1019 (parser.ParseDir in tools/openapigen) and ST1005 (intentional capitalized user-facing error copy that tests assert verbatim). No inline nolint directives were introduced.

Resolve all 217 findings behavior-preserving: gofumpt/goimports formatting, explicit blank assignment on intentionally ignored errors, errors.Is/errors.As and %w wrapping, context-aware stdlib calls (CommandContext/QueryContext/NewRequestWithContext/Dialer), staticcheck simplifications, removed redundant conversions, http.StatusOK and http.MethodGet, inlined the go:fix intPtr helper, and deferred sql rows Close. Add a golangci CI job mirroring the existing Go jobs.
This commit is contained in:
MHSanaei
2026-06-27 15:42:22 +02:00
parent 7efa0d9ddd
commit fa1a19c03c
81 changed files with 410 additions and 286 deletions

View File

@@ -102,6 +102,21 @@ jobs:
go test -run '^$' -fuzz 'FuzzParseLink$' -fuzztime=30s ./internal/util/link/ go test -run '^$' -fuzz 'FuzzParseLink$' -fuzztime=30s ./internal/util/link/
go test -run '^$' -fuzz 'FuzzDecodeCertPin$' -fuzztime=30s ./internal/web/runtime/ go test -run '^$' -fuzz 'FuzzDecodeCertPin$' -fuzztime=30s ./internal/web/runtime/
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Stub internal/web/dist for go:embed
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: latest
frontend: frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

53
.golangci.yml Normal file
View File

@@ -0,0 +1,53 @@
version: "2"
run:
build-tags: []
timeout: 5m
linters:
default: standard
enable:
- bodyclose
- errorlint
- noctx
- misspell
- rowserrcheck
- sqlclosecheck
- unconvert
- usestdlibvars
exclusions:
generated: lax
presets:
- std-error-handling
paths:
- frontend
- internal/web/dist
rules:
- path: _test\.go
linters:
- errcheck
- bodyclose
- noctx
# tools/openapigen relies on go/parser.ParseDir; migrating it to
# golang.org/x/tools/go/packages is a generator change, out of scope here.
- linters:
- staticcheck
text: "SA1019: parser.ParseDir"
# ST1005 (capitalized error strings) conflicts with intentional
# user-facing error copy that tests assert verbatim.
- linters:
- staticcheck
text: "ST1005:"
formatters:
enable:
- gofumpt
- goimports
settings:
goimports:
local-prefixes:
- github.com/mhsanaei/3x-ui
exclusions:
paths:
- frontend
- internal/web/dist

View File

@@ -3,10 +3,11 @@ package database
import ( import (
"testing" "testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
) )
func TestNormalizeApiTokenCreatedAtSeconds(t *testing.T) { func TestNormalizeApiTokenCreatedAtSeconds(t *testing.T) {

View File

@@ -4,6 +4,7 @@ package database
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -43,7 +44,7 @@ func IsPostgres() bool {
if db == nil { if db == nil {
return config.GetDBKind() == "postgres" return config.GetDBKind() == "postgres"
} }
return db.Dialector.Name() == "postgres" return db.Name() == "postgres"
} }
// Dialect returns the active GORM dialect name, or "" if the DB is not open. // Dialect returns the active GORM dialect name, or "" if the DB is not open.
@@ -51,7 +52,7 @@ func Dialect() string {
if db == nil { if db == nil {
return "" return ""
} }
return db.Dialector.Name() return db.Name()
} }
const ( const (
@@ -363,7 +364,6 @@ func initUser() error {
} }
if empty { if empty {
hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword) hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword)
if err != nil { if err != nil {
log.Printf("Error hashing default password: %v", err) log.Printf("Error hashing default password: %v", err)
return err return err
@@ -580,7 +580,7 @@ func fail2banCanEnforce() bool {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return false return false
} }
return exec.Command("fail2ban-client", "-h").Run() == nil return exec.CommandContext(context.Background(), "fail2ban-client", "-h").Run() == nil
} }
// clearLegacyProxySettings drops the deprecated panelProxy/tgBotProxy rows so a // clearLegacyProxySettings drops the deprecated panelProxy/tgBotProxy rows so a
@@ -1038,7 +1038,7 @@ func InitDB(dbPath string) error {
} }
default: default:
dir := path.Dir(dbPath) dir := path.Dir(dbPath)
if err = os.MkdirAll(dir, 0755); err != nil { if err = os.MkdirAll(dir, 0o755); err != nil {
return err return err
} }
// Keep journal_mode=DELETE so the DB stays a single file (no -wal/-shm // Keep journal_mode=DELETE so the DB stays a single file (no -wal/-shm
@@ -1065,7 +1065,7 @@ func InitDB(dbPath string) error {
"PRAGMA temp_store=MEMORY", "PRAGMA temp_store=MEMORY",
} }
for _, p := range pragmas { for _, p := range pragmas {
if _, err := sqlDB.Exec(p); err != nil { if _, err := sqlDB.ExecContext(context.Background(), p); err != nil {
return err return err
} }
} }

View File

@@ -1,6 +1,7 @@
package database package database
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
"os" "os"
@@ -50,23 +51,21 @@ func DumpSQLiteToBytes(srcPath string) ([]byte, error) {
// Tables in creation order, each followed by its data. // Tables in creation order, each followed by its data.
type object struct{ name, ddl string } type object struct{ name, ddl string }
var tables []object var tables []object
rows, err := sqlDB.Query(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL ORDER BY rowid`) rows, err := sqlDB.QueryContext(context.Background(), `SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL ORDER BY rowid`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
for rows.Next() { for rows.Next() {
var o object var o object
if err := rows.Scan(&o.name, &o.ddl); err != nil { if err := rows.Scan(&o.name, &o.ddl); err != nil {
rows.Close()
return nil, err return nil, err
} }
tables = append(tables, o) tables = append(tables, o)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
rows.Close()
return nil, err return nil, err
} }
rows.Close()
for _, t := range tables { for _, t := range tables {
b.WriteString(t.ddl) b.WriteString(t.ddl)
@@ -85,24 +84,22 @@ func DumpSQLiteToBytes(srcPath string) ([]byte, error) {
} }
// Indexes, triggers and views after the data is in place. // Indexes, triggers and views after the data is in place.
rows2, err := sqlDB.Query(`SELECT sql FROM sqlite_master WHERE type IN ('index','trigger','view') AND sql IS NOT NULL ORDER BY rowid`) rows2, err := sqlDB.QueryContext(context.Background(), `SELECT sql FROM sqlite_master WHERE type IN ('index','trigger','view') AND sql IS NOT NULL ORDER BY rowid`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows2.Close()
for rows2.Next() { for rows2.Next() {
var ddl string var ddl string
if err := rows2.Scan(&ddl); err != nil { if err := rows2.Scan(&ddl); err != nil {
rows2.Close()
return nil, err return nil, err
} }
b.WriteString(ddl) b.WriteString(ddl)
b.WriteString(";\n") b.WriteString(";\n")
} }
if err := rows2.Err(); err != nil { if err := rows2.Err(); err != nil {
rows2.Close()
return nil, err return nil, err
} }
rows2.Close()
b.WriteString("COMMIT;\n") b.WriteString("COMMIT;\n")
@@ -131,7 +128,7 @@ func RestoreSQLite(dumpPath, dstPath string) error {
} }
// mattn/go-sqlite3 executes every statement in a multi-statement string. // mattn/go-sqlite3 executes every statement in a multi-statement string.
if _, err := sqlDB.Exec(string(script)); err != nil { if _, err := sqlDB.ExecContext(context.Background(), string(script)); err != nil {
sqlDB.Close() sqlDB.Close()
os.Remove(dstPath) os.Remove(dstPath)
return fmt.Errorf("restore failed: %w", err) return fmt.Errorf("restore failed: %w", err)
@@ -141,7 +138,7 @@ func RestoreSQLite(dumpPath, dstPath string) error {
// dumpTableData appends one INSERT statement per row of table to b. // dumpTableData appends one INSERT statement per row of table to b.
func dumpTableData(db *sql.DB, table string, b *strings.Builder) error { func dumpTableData(db *sql.DB, table string, b *strings.Builder) error {
rows, err := db.Query(`SELECT * FROM "` + table + `"`) rows, err := db.QueryContext(context.Background(), `SELECT * FROM "`+table+`"`)
if err != nil { if err != nil {
return err return err
} }
@@ -213,6 +210,6 @@ func quoteSQLiteText(s string) string {
func sqliteTableExists(db *sql.DB, name string) bool { func sqliteTableExists(db *sql.DB, name string) bool {
var found string var found string
err := db.QueryRow(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, name).Scan(&found) err := db.QueryRowContext(context.Background(), `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, name).Scan(&found)
return err == nil return err == nil
} }

View File

@@ -67,7 +67,7 @@ func MigrateData(srcPath, dstDSN string) error {
return errors.New("destination DSN is required") return errors.New("destination DSN is required")
} }
if err := os.MkdirAll(path.Dir(srcPath), 0755); err != nil { if err := os.MkdirAll(path.Dir(srcPath), 0o755); err != nil {
return err return err
} }
@@ -144,7 +144,7 @@ func ExportPostgresToSQLite(srcDSN, dstPath string) error {
if srcDSN == "" { if srcDSN == "" {
return errors.New("source DSN is required") return errors.New("source DSN is required")
} }
if err := os.MkdirAll(path.Dir(dstPath), 0755); err != nil { if err := os.MkdirAll(path.Dir(dstPath), 0o755); err != nil {
return err return err
} }
// Start from an empty file so AutoMigrate creates the canonical schema. // Start from an empty file so AutoMigrate creates the canonical schema.

View File

@@ -6,8 +6,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/op/go-logging" "github.com/op/go-logging"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@@ -10,9 +10,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/mhsanaei/3x-ui/v3/internal/config"
"github.com/op/go-logging" "github.com/op/go-logging"
"github.com/mhsanaei/3x-ui/v3/internal/config"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )

View File

@@ -2,6 +2,7 @@ package mtproto
import ( import (
"bufio" "bufio"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
@@ -181,7 +182,7 @@ func (m *Manager) ensureLocked(inst Instance) error {
cur.tag = inst.Tag cur.tag = inst.Tag
return nil return nil
} }
cur.proc.Stop() _ = cur.proc.Stop()
delete(m.procs, inst.Id) delete(m.procs, inst.Id)
} }
metricsPort, err := FreeLocalPort() metricsPort, err := FreeLocalPort()
@@ -211,7 +212,7 @@ func (m *Manager) Remove(id int) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if cur, ok := m.procs[id]; ok { if cur, ok := m.procs[id]; ok {
cur.proc.Stop() _ = cur.proc.Stop()
delete(m.procs, id) delete(m.procs, id)
_ = os.Remove(configPathForID(id)) _ = os.Remove(configPathForID(id))
logger.Infof("mtproto: stopped mtg for inbound %d", id) logger.Infof("mtproto: stopped mtg for inbound %d", id)
@@ -231,7 +232,7 @@ func (m *Manager) Reconcile(desired []Instance) {
} }
for id, cur := range m.procs { for id, cur := range m.procs {
if _, ok := want[id]; !ok { if _, ok := want[id]; !ok {
cur.proc.Stop() _ = cur.proc.Stop()
delete(m.procs, id) delete(m.procs, id)
_ = os.Remove(configPathForID(id)) _ = os.Remove(configPathForID(id))
} }
@@ -323,7 +324,7 @@ func (m *Manager) CollectTraffic() []Traffic {
// for mtg's metrics endpoint and to allocate the per-inbound SOCKS egress // for mtg's metrics endpoint and to allocate the per-inbound SOCKS egress
// bridge port persisted into mtproto inbound settings. // bridge port persisted into mtproto inbound settings.
func FreeLocalPort() (int, error) { func FreeLocalPort() (int, error) {
l, err := net.Listen("tcp", "127.0.0.1:0") l, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0")
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -383,7 +384,11 @@ func writeConfig(path string, inst Instance, metricsPort int) error {
// Best-effort: an unreachable endpoint or unrecognised format yields ok=false. // Best-effort: an unreachable endpoint or unrecognised format yields ok=false.
func scrapeTraffic(port int) (up int64, down int64, ok bool) { func scrapeTraffic(port int) (up int64, down int64, ok bool) {
client := http.Client{Timeout: 3 * time.Second} client := http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", port)) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/metrics", port), nil)
if reqErr != nil {
return 0, 0, false
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return 0, 0, false return 0, 0, false
} }
@@ -418,7 +423,7 @@ func scrapeTraffic(port int) (up int64, down int64, ok bool) {
func parseMetricLine(line string) (name string, labels map[string]string, value float64, err error) { func parseMetricLine(line string) (name string, labels map[string]string, value float64, err error) {
labels = map[string]string{} labels = map[string]string{}
rest := line var rest string
if brace := strings.IndexByte(line, '{'); brace >= 0 { if brace := strings.IndexByte(line, '{'); brace >= 0 {
name = line[:brace] name = line[:brace]
end := strings.IndexByte(line, '}') end := strings.IndexByte(line, '}')

View File

@@ -5,6 +5,7 @@
package mtproto package mtproto
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -154,7 +155,7 @@ func (p *Process) Start() error {
if p.IsRunning() { if p.IsRunning() {
return errors.New("mtg is already running") return errors.New("mtg is already running")
} }
cmd := exec.Command(GetBinaryPath(), "run", p.configPath) cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "run", p.configPath)
cmd.Stdout = p.logWriter cmd.Stdout = p.logWriter
cmd.Stderr = p.logWriter cmd.Stderr = p.logWriter
p.cmd = cmd p.cmd = cmd

View File

@@ -7,8 +7,9 @@ import (
"sync" "sync"
"unsafe" "unsafe"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
) )
var ( var (
@@ -36,7 +37,7 @@ func ensureKillOnExitJob() (windows.Handle, error) {
uint32(unsafe.Sizeof(info)), uint32(unsafe.Sizeof(info)),
) )
if err != nil { if err != nil {
windows.CloseHandle(h) _ = windows.CloseHandle(h)
killOnExitJobErr = err killOnExitJobErr = err
return return
} }
@@ -59,7 +60,7 @@ func attachChildLifetime(cmd *exec.Cmd) {
logger.Warningf("mtproto: OpenProcess for job attach failed: %v", err) logger.Warningf("mtproto: OpenProcess for job attach failed: %v", err)
return return
} }
defer windows.CloseHandle(h) defer func() { _ = windows.CloseHandle(h) }()
if err := windows.AssignProcessToJobObject(job, h); err != nil { if err := windows.AssignProcessToJobObject(job, h); err != nil {
logger.Warningf("mtproto: AssignProcessToJobObject failed: %v", err) logger.Warningf("mtproto: AssignProcessToJobObject failed: %v", err)
} }

View File

@@ -241,7 +241,7 @@ func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound,
proxy["type"] = "vless" proxy["type"] = "vless"
proxy["uuid"] = client.ID proxy["uuid"] = client.ID
var inboundSettings map[string]any var inboundSettings map[string]any
json.Unmarshal([]byte(inbound.Settings), &inboundSettings) _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
streamSecurity, _ := stream["security"].(string) streamSecurity, _ := stream["security"].(string)
if client.Flow != "" && vlessFlowAllowed(network, streamSecurity, inboundSettings) { if client.Flow != "" && vlessFlowAllowed(network, streamSecurity, inboundSettings) {
proxy["flow"] = client.Flow proxy["flow"] = client.Flow
@@ -259,7 +259,7 @@ func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound,
proxy["type"] = "ss" proxy["type"] = "ss"
proxy["password"] = client.Password proxy["password"] = client.Password
var inboundSettings map[string]any var inboundSettings map[string]any
json.Unmarshal([]byte(inbound.Settings), &inboundSettings) _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
method, _ := inboundSettings["method"].(string) method, _ := inboundSettings["method"].(string)
if method == "" { if method == "" {
return nil return nil
@@ -655,7 +655,7 @@ func (s *SubClashService) applySecurity(proxy map[string]any, security string, s
func (s *SubClashService) streamData(stream string) map[string]any { func (s *SubClashService) streamData(stream string) map[string]any {
var streamSettings map[string]any var streamSettings map[string]any
json.Unmarshal([]byte(stream), &streamSettings) _ = json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string) security, _ := streamSettings["security"].(string)
switch security { switch security {
case "tls": case "tls":

View File

@@ -16,6 +16,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/web/service" "github.com/mhsanaei/3x-ui/v3/internal/web/service"
) )
@@ -417,7 +418,7 @@ func (a *SUBController) ApplyCommonHeaders(
c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", updateInterval) c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
//Basics // Basics
if profileTitle != "" { if profileTitle != "" {
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle))) c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
} }
@@ -431,7 +432,7 @@ func (a *SUBController) ApplyCommonHeaders(
c.Writer.Header().Set("Announce", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce))) c.Writer.Header().Set("Announce", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce)))
} }
//Advanced (Happ) // Advanced (Happ)
c.Writer.Header().Set("Routing-Enable", strconv.FormatBool(profileEnableRouting)) c.Writer.Header().Set("Routing-Enable", strconv.FormatBool(profileEnableRouting))
if profileRoutingRules != "" { if profileRoutingRules != "" {
c.Writer.Header().Set("Routing", profileRoutingRules) c.Writer.Header().Set("Routing", profileRoutingRules)

View File

@@ -1,6 +1,7 @@
package sub package sub
import ( import (
"context"
"encoding/base64" "encoding/base64"
"io" "io"
"net/http" "net/http"
@@ -64,7 +65,7 @@ func fetchSubscriptionLinks(rawURL string) []string {
} }
func doFetchSubscriptionLinks(rawURL string) ([]string, error) { func doFetchSubscriptionLinks(rawURL string) ([]string, error) {
req, err := http.NewRequest(http.MethodGet, rawURL, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, rawURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,6 +1,7 @@
package sub package sub
import ( import (
"errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@@ -14,7 +15,7 @@ func TestDoFetchSubscriptionLinks_RejectsOversizedBody(t *testing.T) {
defer srv.Close() defer srv.Close()
links, err := doFetchSubscriptionLinks(srv.URL) links, err := doFetchSubscriptionLinks(srv.URL)
if err != errSubscriptionBodyTooLarge { if !errors.Is(err, errSubscriptionBodyTooLarge) {
t.Fatalf("err = %v, want errSubscriptionBodyTooLarge", err) t.Fatalf("err = %v, want errSubscriptionBodyTooLarge", err)
} }
if links != nil { if links != nil {

View File

@@ -29,7 +29,7 @@ type SubJsonService struct {
func NewSubJsonService(mux string, rules string, finalMask string, subService *SubService) *SubJsonService { func NewSubJsonService(mux string, rules string, finalMask string, subService *SubService) *SubJsonService {
var configJson map[string]any var configJson map[string]any
var defaultOutbounds []json_util.RawMessage var defaultOutbounds []json_util.RawMessage
json.Unmarshal([]byte(defaultJson), &configJson) _ = json.Unmarshal([]byte(defaultJson), &configJson)
if outboundSlices, ok := configJson["outbounds"].([]any); ok { if outboundSlices, ok := configJson["outbounds"].([]any); ok {
for _, defaultOutbound := range outboundSlices { for _, defaultOutbound := range outboundSlices {
jsonBytes, _ := json.Marshal(defaultOutbound) jsonBytes, _ := json.Marshal(defaultOutbound)
@@ -41,7 +41,7 @@ func NewSubJsonService(mux string, rules string, finalMask string, subService *S
var newRules []any var newRules []any
routing, _ := configJson["routing"].(map[string]any) routing, _ := configJson["routing"].(map[string]any)
defaultRules, _ := routing["rules"].([]any) defaultRules, _ := routing["rules"].([]any)
json.Unmarshal([]byte(rules), &newRules) _ = json.Unmarshal([]byte(rules), &newRules)
defaultRules = append(newRules, defaultRules...) defaultRules = append(newRules, defaultRules...)
routing["rules"] = defaultRules routing["rules"] = defaultRules
configJson["routing"] = routing configJson["routing"] = routing
@@ -234,7 +234,7 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c
func (s *SubJsonService) streamData(stream string) map[string]any { func (s *SubJsonService) streamData(stream string) map[string]any {
var streamSettings map[string]any var streamSettings map[string]any
json.Unmarshal([]byte(stream), &streamSettings) _ = json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string) security, _ := streamSettings["security"].(string)
switch security { switch security {
case "tls": case "tls":
@@ -392,7 +392,7 @@ func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_ut
// Add encryption for VLESS outbound from inbound settings // Add encryption for VLESS outbound from inbound settings
var inboundSettings map[string]any var inboundSettings map[string]any
json.Unmarshal([]byte(inbound.Settings), &inboundSettings) _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
encryption, _ := inboundSettings["encryption"].(string) encryption, _ := inboundSettings["encryption"].(string)
settings := map[string]any{ settings := map[string]any{
@@ -423,7 +423,7 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
if inbound.Protocol == model.Shadowsocks { if inbound.Protocol == model.Shadowsocks {
var inboundSettings map[string]any var inboundSettings map[string]any
json.Unmarshal([]byte(inbound.Settings), &inboundSettings) _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
method, _ := inboundSettings["method"].(string) method, _ := inboundSettings["method"].(string)
serverData[0].Method = method serverData[0].Method = method
@@ -474,7 +474,7 @@ func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any,
} }
var settings, stream map[string]any var settings, stream map[string]any
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
version, _ := settings["version"].(float64) version, _ := settings["version"].(float64)
outbound.Settings = map[string]any{ outbound.Settings = map[string]any{
"version": int(version), "version": int(version),
@@ -482,7 +482,7 @@ func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any,
"port": inbound.Port, "port": inbound.Port,
} }
json.Unmarshal([]byte(inbound.StreamSettings), &stream) _ = json.Unmarshal([]byte(inbound.StreamSettings), &stream)
hyStream := stream["hysteriaSettings"].(map[string]any) hyStream := stream["hysteriaSettings"].(map[string]any)
outHyStream := map[string]any{ outHyStream := map[string]any{
"version": int(version), "version": int(version),

View File

@@ -453,12 +453,12 @@ func (s *SubService) projectThroughFallbackMaster(inbound *model.Inbound) bool {
// + ws/grpc/etc. settings) stays the child's. // + ws/grpc/etc. settings) stays the child's.
func mergeStreamFromMaster(childStream, masterStream string) string { func mergeStreamFromMaster(childStream, masterStream string) string {
var stream map[string]any var stream map[string]any
json.Unmarshal([]byte(childStream), &stream) _ = json.Unmarshal([]byte(childStream), &stream)
if stream == nil { if stream == nil {
stream = map[string]any{} stream = map[string]any{}
} }
var mst map[string]any var mst map[string]any
json.Unmarshal([]byte(masterStream), &mst) _ = json.Unmarshal([]byte(masterStream), &mst)
if mst == nil { if mst == nil {
return childStream return childStream
} }
@@ -511,7 +511,7 @@ func (s *SubService) genMtprotoLink(inbound *model.Inbound, _ string) string {
return "" return ""
} }
settings := map[string]any{} settings := map[string]any{}
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
secret, _ := settings["secret"].(string) secret, _ := settings["secret"].(string)
if secret == "" { if secret == "" {
if healed, ok := model.HealMtprotoSecret(inbound.Settings); ok { if healed, ok := model.HealMtprotoSecret(inbound.Settings); ok {
@@ -617,7 +617,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
// Add encryption parameter for VLESS from inbound settings // Add encryption parameter for VLESS from inbound settings
var settings map[string]any var settings map[string]any
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
if encryption, ok := settings["encryption"].(string); ok { if encryption, ok := settings["encryption"].(string); ok {
params["encryption"] = encryption params["encryption"] = encryption
} }
@@ -739,7 +739,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
clients, _ := s.inboundService.GetClients(inbound) clients, _ := s.inboundService.GetClients(inbound)
var settings map[string]any var settings map[string]any
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
inboundPassword := settings["password"].(string) inboundPassword := settings["password"].(string)
method := settings["method"].(string) method := settings["method"].(string)
clientIndex := findClientIndex(clients, email) clientIndex := findClientIndex(clients, email)
@@ -808,7 +808,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
return "" return ""
} }
var stream map[string]any var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream) _ = json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound) clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1 clientIndex := -1
for i, client := range clients { for i, client := range clients {
@@ -877,7 +877,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
} }
var settings map[string]any var settings map[string]any
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
version, _ := settings["version"].(float64) version, _ := settings["version"].(float64)
protocol := "hysteria2" protocol := "hysteria2"
if int(version) == 1 { if int(version) == 1 {
@@ -1008,7 +1008,7 @@ func findClientIndex(clients []model.Client, email string) int {
func unmarshalStreamSettings(streamSettings string) map[string]any { func unmarshalStreamSettings(streamSettings string) map[string]any {
var stream map[string]any var stream map[string]any
json.Unmarshal([]byte(streamSettings), &stream) _ = json.Unmarshal([]byte(streamSettings), &stream)
return stream return stream
} }
@@ -1316,7 +1316,7 @@ func buildVmessLink(obj map[string]any) string {
func cloneVmessShareObj(baseObj map[string]any, newSecurity string) map[string]any { func cloneVmessShareObj(baseObj map[string]any, newSecurity string) map[string]any {
newObj := map[string]any{} newObj := map[string]any{}
for key, value := range baseObj { for key, value := range baseObj {
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "pcs")) { if newSecurity != "none" || (key != "alpn" && key != "sni" && key != "fp" && key != "pcs") {
newObj[key] = value newObj[key] = value
} }
} }

View File

@@ -253,7 +253,7 @@ func (s *Server) Start() (err error) {
// This is an anonymous function, no function name // This is an anonymous function, no function name
defer func() { defer func() {
if err != nil { if err != nil {
s.Stop() _ = s.Stop()
} }
}() }()
@@ -288,7 +288,7 @@ func (s *Server) Start() (err error) {
} }
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
listener, err := net.Listen("tcp", listenAddr) listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", listenAddr)
if err != nil { if err != nil {
return err return err
} }
@@ -323,7 +323,7 @@ func (s *Server) Start() (err error) {
} }
go func() { go func() {
s.httpServer.Serve(listener) _ = s.httpServer.Serve(listener)
}() }()
return nil return nil

View File

@@ -189,7 +189,7 @@ func (m *Monitor) Step(ctx context.Context) (bool, error) {
} }
if recErr := m.recover(ctx); recErr != nil { if recErr := m.recover(ctx); recErr != nil {
return false, fmt.Errorf("recovery failed after probe error %v: %w", err, recErr) return false, fmt.Errorf("recovery failed after probe error %w: %w", err, recErr)
} }
m.lastRecovery = now m.lastRecovery = now

View File

@@ -9,8 +9,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/op/go-logging" "github.com/op/go-logging"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@@ -5,8 +5,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/op/go-logging" "github.com/op/go-logging"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@@ -15,7 +15,7 @@ func TestSeq_LengthAndAlphabet(t *testing.T) {
isDigit := r >= '0' && r <= '9' isDigit := r >= '0' && r <= '9'
isLower := r >= 'a' && r <= 'z' isLower := r >= 'a' && r <= 'z'
isUpper := r >= 'A' && r <= 'Z' isUpper := r >= 'A' && r <= 'Z'
if !(isDigit || isLower || isUpper) { if !isDigit && !isLower && !isUpper {
t.Fatalf("Seq(%d) byte %d = %q is not alphanumeric", n, i, r) t.Fatalf("Seq(%d) byte %d = %q is not alphanumeric", n, i, r)
} }
} }

View File

@@ -66,7 +66,8 @@ func CPUPercentRaw() (float64, error) {
uintptr(unsafe.Pointer(&userFT)), uintptr(unsafe.Pointer(&userFT)),
) )
if r1 == 0 { if r1 == 0 {
if errno, _ := e1.(syscall.Errno); errno != 0 { var errno syscall.Errno
if errors.As(e1, &errno) && errno != 0 {
return 0, errno return 0, errno
} }
return 0, errors.New("GetSystemTimes failed") return 0, errors.New("GetSystemTimes failed")

View File

@@ -5,7 +5,6 @@ import (
"strings" "strings"
"github.com/mhsanaei/3x-ui/v3/internal/web/middleware" "github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel" "github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
"github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot" "github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot"
"github.com/mhsanaei/3x-ui/v3/internal/web/session" "github.com/mhsanaei/3x-ui/v3/internal/web/session"
@@ -22,7 +21,6 @@ type APIController struct {
hostController *HostController hostController *HostController
settingController *SettingController settingController *SettingController
xraySettingController *XraySettingController xraySettingController *XraySettingController
settingService service.SettingService
userService panel.UserService userService panel.UserService
apiTokenService panel.ApiTokenService apiTokenService panel.ApiTokenService
Tgbot tgbot.Tgbot Tgbot tgbot.Tgbot

View File

@@ -61,7 +61,6 @@ func (a *InboundController) broadcastInboundsUpdate(userId int) {
// initRouter initializes the routes for inbound-related operations. // initRouter initializes the routes for inbound-related operations.
func (a *InboundController) initRouter(g *gin.RouterGroup) { func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.GET("/list", a.getInbounds) g.GET("/list", a.getInbounds)
g.GET("/list/slim", a.getInboundsSlim) g.GET("/list/slim", a.getInboundsSlim)
g.GET("/options", a.getInboundOptions) g.GET("/options", a.getInboundOptions)

View File

@@ -42,7 +42,6 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
// initRouter sets up the routes for server status, Xray management, and utility endpoints. // initRouter sets up the routes for server status, Xray management, and utility endpoints.
func (a *ServerController) initRouter(g *gin.RouterGroup) { func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.GET("/status", a.status) g.GET("/status", a.status)
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket)
g.GET("/history/:metric/:bucket", a.getMetricHistoryBucket) g.GET("/history/:metric/:bucket", a.getMetricHistoryBucket)
@@ -89,7 +88,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
// the cross-service side effects (xrayMetrics sample + websocket broadcast). // the cross-service side effects (xrayMetrics sample + websocket broadcast).
func (a *ServerController) startTask() { func (a *ServerController) startTask() {
c := global.GetWebServer().GetCron() c := global.GetWebServer().GetCron()
c.AddFunc("@every 2s", func() { _, _ = c.AddFunc("@every 2s", func() {
status := a.serverService.RefreshStatus() status := a.serverService.RefreshStatus()
if status == nil { if status == nil {
return return
@@ -97,7 +96,7 @@ func (a *ServerController) startTask() {
a.xrayMetricsService.Sample(time.Now()) a.xrayMetricsService.Sample(time.Now())
websocket.BroadcastStatus(status) websocket.BroadcastStatus(status)
}) })
c.AddFunc("@every 1m", func() { _, _ = c.AddFunc("@every 1m", func() {
if err := service.PersistSystemMetrics(); err != nil { if err := service.PersistSystemMetrics(); err != nil {
logger.Warning("persist system metrics failed:", err) logger.Warning("persist system metrics failed:", err)
} }
@@ -327,13 +326,13 @@ func (a *ServerController) getDb(c *gin.Context) {
filename := a.serverService.BackupFilename(c.Request.Host) filename := a.serverService.BackupFilename(c.Request.Host)
if !filenameRegex.MatchString(filename) { if !filenameRegex.MatchString(filename) {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename")) _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
return return
} }
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+filename) c.Header("Content-Disposition", "attachment; filename="+filename)
c.Writer.Write(db) _, _ = c.Writer.Write(db)
} }
// getMigration downloads a cross-engine migration file: a .dump on SQLite or a // getMigration downloads a cross-engine migration file: a .dump on SQLite or a
@@ -345,13 +344,13 @@ func (a *ServerController) getMigration(c *gin.Context) {
return return
} }
if !filenameRegex.MatchString(filename) { if !filenameRegex.MatchString(filename) {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename")) _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
return return
} }
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+filename) c.Header("Content-Disposition", "attachment; filename="+filename)
c.Writer.Write(data) _, _ = c.Writer.Write(data)
} }
// importDB imports a database file and restarts the Xray service. // importDB imports a database file and restarts the Xray service.

View File

@@ -1,6 +1,7 @@
package job package job
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"log" "log"
@@ -127,7 +128,7 @@ func (j *CheckClientIpJob) hasLimitIp() bool {
} }
settings := map[string][]model.Client{} settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"] clients := settings["clients"]
for _, client := range clients { for _, client := range clients {
@@ -189,7 +190,7 @@ func (j *CheckClientIpJob) processObserved(observed map[string]map[string]int64,
clientIpsRecord, err := j.getInboundClientIps(email) clientIpsRecord, err := j.getInboundClientIps(email)
if err != nil { if err != nil {
j.addInboundClientIps(email, ipsWithTime) _ = j.addInboundClientIps(email, ipsWithTime)
continue continue
} }
@@ -277,7 +278,7 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() bool {
cmd := "fail2ban-client" cmd := "fail2ban-client"
args := []string{"-h"} args := []string{"-h"}
err := exec.Command(cmd, args...).Run() err := exec.CommandContext(context.Background(), cmd, args...).Run()
return err == nil return err == nil
} }
@@ -345,7 +346,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
} }
settings := map[string][]model.Client{} settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"] clients := settings["clients"]
// Find the client's IP limit // Find the client's IP limit
@@ -372,7 +373,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
// Parse old IPs from database // Parse old IPs from database
var oldIpsWithTime []IPWithTimestamp var oldIpsWithTime []IPWithTimestamp
if inboundClientIps.Ips != "" { if inboundClientIps.Ips != "" {
json.Unmarshal([]byte(inboundClientIps.Ips), &oldIpsWithTime) _ = json.Unmarshal([]byte(inboundClientIps.Ips), &oldIpsWithTime)
} }
ipMap := mergeClientIps(oldIpsWithTime, newIpsWithTime, time.Now().Unix()-ipStaleAfterSeconds, observedAreLive) ipMap := mergeClientIps(oldIpsWithTime, newIpsWithTime, time.Now().Unix()-ipStaleAfterSeconds, observedAreLive)
@@ -393,7 +394,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
if len(bannedLive) > 0 { if len(bannedLive) > 0 {
shouldCleanLog = true shouldCleanLog = true
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil { if err != nil {
logger.Errorf("failed to open IP limit log file: %s", err) logger.Errorf("failed to open IP limit log file: %s", err)
return false return false
@@ -455,7 +456,7 @@ func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, c
if client.Email == clientEmail { if client.Email == clientEmail {
// Convert client to map for API // Convert client to map for API
clientBytes, _ := json.Marshal(client) clientBytes, _ := json.Marshal(client)
json.Unmarshal(clientBytes, &clientConfig) _ = json.Unmarshal(clientBytes, &clientConfig)
break break
} }
} }

View File

@@ -9,10 +9,11 @@ import (
"testing" "testing"
"time" "time"
"github.com/op/go-logging"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/op/go-logging"
) )
// 3x-ui logger must be initialised once before any code path that can // 3x-ui logger must be initialised once before any code path that can

View File

@@ -4,15 +4,12 @@ import (
"time" "time"
"github.com/mhsanaei/3x-ui/v3/internal/eventbus" "github.com/mhsanaei/3x-ui/v3/internal/eventbus"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
) )
// CheckCpuJob monitors CPU usage and publishes events when threshold is exceeded. // CheckCpuJob monitors CPU usage and publishes events when threshold is exceeded.
type CheckCpuJob struct { type CheckCpuJob struct{}
settingService service.SettingService
}
// NewCheckCpuJob creates a new CPU monitoring job instance. // NewCheckCpuJob creates a new CPU monitoring job instance.
func NewCheckCpuJob() *CheckCpuJob { func NewCheckCpuJob() *CheckCpuJob {

View File

@@ -2,15 +2,12 @@ package job
import ( import (
"github.com/mhsanaei/3x-ui/v3/internal/eventbus" "github.com/mhsanaei/3x-ui/v3/internal/eventbus"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
"github.com/shirou/gopsutil/v4/mem" "github.com/shirou/gopsutil/v4/mem"
) )
// CheckMemJob monitors memory usage and publishes events when threshold is exceeded. // CheckMemJob monitors memory usage and publishes events when threshold is exceeded.
type CheckMemJob struct { type CheckMemJob struct{}
settingService service.SettingService
}
// NewCheckMemJob creates a new memory monitoring job instance. // NewCheckMemJob creates a new memory monitoring job instance.
func NewCheckMemJob() *CheckMemJob { func NewCheckMemJob() *CheckMemJob {

View File

@@ -20,11 +20,11 @@ func NewClearLogsJob() *ClearLogsJob {
// ensureFileExists creates the necessary directories and file if they don't exist // ensureFileExists creates the necessary directories and file if they don't exist
func ensureFileExists(path string) error { func ensureFileExists(path string) error {
dir := filepath.Dir(path) dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0o755); err != nil {
return err return err
} }
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644) file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644)
if err != nil { if err != nil {
return err return err
} }
@@ -48,13 +48,13 @@ func (j *ClearLogsJob) Run() {
for i := range len(logFiles) { for i := range len(logFiles) {
if i > 0 { if i > 0 {
// Copy to previous logs // Copy to previous logs
logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil { if err != nil {
logger.Warning("Failed to open previous log file for writing:", logFilesPrev[i-1], "-", err) logger.Warning("Failed to open previous log file for writing:", logFilesPrev[i-1], "-", err)
continue continue
} }
logFile, err := os.OpenFile(logFiles[i], os.O_RDONLY, 0644) logFile, err := os.OpenFile(logFiles[i], os.O_RDONLY, 0o644)
if err != nil { if err != nil {
logger.Warning("Failed to open current log file for reading:", logFiles[i], "-", err) logger.Warning("Failed to open current log file for reading:", logFiles[i], "-", err)
logFilePrev.Close() logFilePrev.Close()

View File

@@ -19,14 +19,14 @@ func writeAccessLogConfig(t *testing.T, accessPath string) {
if err != nil { if err != nil {
t.Fatalf("marshal xray config: %v", err) t.Fatalf("marshal xray config: %v", err)
} }
if err := os.WriteFile(filepath.Join(binDir, "config.json"), configData, 0644); err != nil { if err := os.WriteFile(filepath.Join(binDir, "config.json"), configData, 0o644); err != nil {
t.Fatalf("write xray config: %v", err) t.Fatalf("write xray config: %v", err)
} }
} }
func TestWipeAccessLog_TruncatesEnabledLog(t *testing.T) { func TestWipeAccessLog_TruncatesEnabledLog(t *testing.T) {
accessLog := filepath.Join(t.TempDir(), "access.log") accessLog := filepath.Join(t.TempDir(), "access.log")
if err := os.WriteFile(accessLog, []byte("2026/06/23 12:00:00 from tcp:203.0.113.10:443 accepted\n"), 0644); err != nil { if err := os.WriteFile(accessLog, []byte("2026/06/23 12:00:00 from tcp:203.0.113.10:443 accepted\n"), 0o644); err != nil {
t.Fatalf("seed access log: %v", err) t.Fatalf("seed access log: %v", err)
} }
writeAccessLogConfig(t, accessLog) writeAccessLogConfig(t, accessLog)

View File

@@ -178,7 +178,7 @@ func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traf
defer fasthttp.ReleaseRequest(request) defer fasthttp.ReleaseRequest(request)
request.Header.SetMethod("POST") request.Header.SetMethod("POST")
request.Header.SetContentType("application/json; charset=UTF-8") request.Header.SetContentType("application/json; charset=UTF-8")
request.SetBody([]byte(requestBody)) request.SetBody(requestBody)
request.SetRequestURI(informURL) request.SetRequestURI(informURL)
response := fasthttp.AcquireResponse() response := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(response) defer fasthttp.ReleaseResponse(response)

View File

@@ -56,7 +56,7 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
// createTemplateData creates a template data map from parameters with optional separator. // createTemplateData creates a template data map from parameters with optional separator.
func createTemplateData(params []string, separator ...string) map[string]any { func createTemplateData(params []string, separator ...string) map[string]any {
var sep string = "==" sep := "=="
if len(separator) > 0 { if len(separator) > 0 {
sep = separator[0] sep = separator[0]
} }

View File

@@ -50,7 +50,7 @@ func (c *AutoHttpsConn) readRequest() bool {
resp.StatusCode = http.StatusTemporaryRedirect resp.StatusCode = http.StatusTemporaryRedirect
location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI) location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
resp.Header.Set("Location", location) resp.Header.Set("Location", location)
resp.Write(c.Conn) _ = resp.Write(c.Conn)
c.Close() c.Close()
c.firstBuf = nil c.firstBuf = nil
return true return true

View File

@@ -34,10 +34,14 @@ func TestResetClientExpiryTimeByEmail_MultiInbound(t *testing.T) {
return string(b) return string(b)
} }
first := &model.Inbound{Tag: "vless-a", Enable: true, Port: 50001, Protocol: model.VLESS, first := &model.Inbound{
StreamSettings: `{"network":"tcp","security":"reality"}`, Settings: clientJSON(oldExpiry)} Tag: "vless-a", Enable: true, Port: 50001, Protocol: model.VLESS,
second := &model.Inbound{Tag: "vless-b", Enable: true, Port: 50002, Protocol: model.VLESS, StreamSettings: `{"network":"tcp","security":"reality"}`, Settings: clientJSON(oldExpiry),
StreamSettings: `{"network":"ws","security":"tls"}`, Settings: clientJSON(oldExpiry)} }
second := &model.Inbound{
Tag: "vless-b", Enable: true, Port: 50002, Protocol: model.VLESS,
StreamSettings: `{"network":"ws","security":"tls"}`, Settings: clientJSON(oldExpiry),
}
for _, ib := range []*model.Inbound{first, second} { for _, ib := range []*model.Inbound{first, second} {
if err := db.Create(ib).Error; err != nil { if err := db.Create(ib).Error; err != nil {
t.Fatalf("create inbound %s: %v", ib.Tag, err) t.Fatalf("create inbound %s: %v", ib.Tag, err)

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/logger"

View File

@@ -40,8 +40,10 @@ func flowOf(t *testing.T, svc *ClientService, email string) string {
return rec.Flow return rec.Flow
} }
const realityStream = `{"network":"tcp","security":"reality"}` const (
const wsStream = `{"network":"ws","security":"none"}` realityStream = `{"network":"tcp","security":"reality"}`
wsStream = `{"network":"ws","security":"none"}`
)
// TestBulkAdjust_FlowSetAndClear covers the happy path: a vision flow is applied // TestBulkAdjust_FlowSetAndClear covers the happy path: a vision flow is applied
// on an eligible VLESS inbound and later cleared with the "none" directive. Both // on an eligible VLESS inbound and later cleared with the "none" directive. Both

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/common" "github.com/mhsanaei/3x-ui/v3/internal/util/common"

View File

@@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/xray" "github.com/mhsanaei/3x-ui/v3/internal/xray"

View File

@@ -84,7 +84,7 @@ func (s *ClientService) BulkResetTraffic(inboundSvc *InboundService, emails []st
if err == nil && !rec.Enable { if err == nil && !rec.Enable {
updated := rec.ToClient() updated := rec.ToClient()
updated.Enable = true updated.Enable = true
s.Update(inboundSvc, rec.Id, *updated) _, _ = s.Update(inboundSvc, rec.Id, *updated)
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
) )

View File

@@ -1,6 +1,7 @@
package email package email
import ( import (
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net" "net"
@@ -115,10 +116,10 @@ func (s *EmailService) TestConnection() SMTPTestResult {
switch encryptionType { switch encryptionType {
case "tls": case "tls":
conn, err = tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{ conn, err = (&tls.Dialer{NetDialer: dialer, Config: &tls.Config{
ServerName: host, ServerName: host,
InsecureSkipVerify: false, InsecureSkipVerify: false,
}) }}).DialContext(context.Background(), "tcp", addr)
default: default:
conn, err = dialer.Dial("tcp", addr) conn, err = dialer.Dial("tcp", addr)
} }
@@ -188,10 +189,10 @@ func (s *EmailService) TestConnection() SMTPTestResult {
func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte, host string) error { func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte, host string) error {
// Dial with explicit timeout // Dial with explicit timeout
dialer := &net.Dialer{Timeout: 10 * time.Second} dialer := &net.Dialer{Timeout: 10 * time.Second}
conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{ conn, err := (&tls.Dialer{NetDialer: dialer, Config: &tls.Config{
ServerName: host, ServerName: host,
InsecureSkipVerify: false, InsecureSkipVerify: false,
}) }}).DialContext(context.Background(), "tcp", addr)
if err != nil { if err != nil {
return err return err
} }
@@ -289,7 +290,7 @@ func buildMessage(from string, to []string, subject, body string) []byte {
} }
var msg strings.Builder var msg strings.Builder
for k, v := range headers { for k, v := range headers {
msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v)) fmt.Fprintf(&msg, "%s: %s\r\n", k, v)
} }
msg.WriteString("\r\n") msg.WriteString("\r\n")
msg.WriteString(body) msg.WriteString(body)

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
@@ -46,7 +47,7 @@ func (s *FallbackService) GetParentForChild(childId int) (*model.InboundFallback
Where("child_id = ?", childId). Where("child_id = ?", childId).
Order("sort_order ASC, id ASC"). Order("sort_order ASC, id ASC").
First(&row).Error First(&row).Error
if err == gorm.ErrRecordNotFound { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil return nil, nil
} }
if err != nil { if err != nil {

View File

@@ -5,6 +5,7 @@ package service
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net" "net"
"sort" "sort"
@@ -153,7 +154,7 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Order("id ASC").Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Order("id ASC").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
s.enrichClientStats(db, inbounds) s.enrichClientStats(db, inbounds)
@@ -196,7 +197,7 @@ func (s *InboundService) GetInboundsSlim(userId int) ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Order("id ASC").Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Order("id ASC").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
s.annotateFallbackParents(db, inbounds) s.annotateFallbackParents(db, inbounds)
@@ -319,7 +320,7 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
Where("user_id = ?", userId). Where("user_id = ?", userId).
Order("id ASC"). Order("id ASC").
Scan(&rows).Error Scan(&rows).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
out := make([]InboundOption, 0, len(rows)) out := make([]InboundOption, 0, len(rows))
@@ -343,7 +344,7 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
s.enrichClientStats(db, inbounds) s.enrichClientStats(db, inbounds)
@@ -354,7 +355,7 @@ func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbo
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("traffic_reset = ?", period).Find(&inbounds).Error err := db.Model(model.Inbound{}).Where("traffic_reset = ?", period).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
return inbounds, nil return inbounds, nil
@@ -362,7 +363,7 @@ func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbo
func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) { func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) {
settings := map[string][]model.Client{} settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
if settings == nil { if settings == nil {
return nil, fmt.Errorf("setting is null") return nil, fmt.Errorf("setting is null")
} }
@@ -1348,7 +1349,7 @@ func (s *InboundService) GetInboundTags() (string, error) {
db := database.GetDB() db := database.GetDB()
var inboundTags []string var inboundTags []string
err := db.Model(model.Inbound{}).Select("tag").Find(&inboundTags).Error err := db.Model(model.Inbound{}).Select("tag").Find(&inboundTags).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err return "", err
} }
tags, _ := json.Marshal(inboundTags) tags, _ := json.Marshal(inboundTags)
@@ -1359,7 +1360,7 @@ func (s *InboundService) GetClientReverseTags() (string, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []model.Inbound var inbounds []model.Inbound
err := db.Model(model.Inbound{}).Select("settings").Where("protocol = ?", "vless").Find(&inbounds).Error err := db.Model(model.Inbound{}).Select("settings").Where("protocol = ?", "vless").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "[]", err return "[]", err
} }
@@ -1404,7 +1405,7 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
return inbounds, nil return inbounds, nil

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/logger"

View File

@@ -27,7 +27,7 @@ func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error
if err != nil { if err != nil {
return false, 0, err return false, 0, err
} }
s.xrayApi.Init(p.GetAPIPort()) _ = s.xrayApi.Init(p.GetAPIPort())
for _, tag := range tags { for _, tag := range tags {
err1 := s.xrayApi.DelInbound(tag) err1 := s.xrayApi.DelInbound(tag)
if err1 == nil { if err1 == nil {
@@ -141,7 +141,7 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, []int,
} }
if p != nil && len(localTargets) > 0 { if p != nil && len(localTargets) > 0 {
s.xrayApi.Init(p.GetAPIPort()) _ = s.xrayApi.Init(p.GetAPIPort())
for _, t := range localTargets { for _, t := range localTargets {
err1 := s.xrayApi.RemoveUser(t.Tag, t.Email) err1 := s.xrayApi.RemoveUser(t.Tag, t.Email)
if err1 == nil { if err1 == nil {
@@ -231,7 +231,7 @@ func (s *InboundService) markClientsDisabledInSettings(tx *gorm.DB, inboundID in
if _, hit := emails[email]; !hit { if _, hit := emails[email]; !hit {
continue continue
} }
if cur, _ := entry["enable"].(bool); cur == false { if cur, _ := entry["enable"].(bool); !cur {
continue continue
} }
entry["enable"] = false entry["enable"] = false

View File

@@ -2,6 +2,7 @@ package service
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@@ -93,12 +94,12 @@ func (s *InboundService) MigrationRequirements() {
// Fix inbounds based problems // Fix inbounds based problems
var inbounds []*model.Inbound var inbounds []*model.Inbound
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria"}).Find(&inbounds).Error err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return return
} }
for inbound_index := range inbounds { for inbound_index := range inbounds {
settings := map[string]any{} settings := map[string]any{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) _ = json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
if raw, exists := settings["clients"]; exists && raw == nil { if raw, exists := settings["clients"]; exists && raw == nil {
settings["clients"] = []any{} settings["clients"] = []any{}
} }
@@ -117,7 +118,7 @@ func (s *InboundService) MigrationRequirements() {
// Convert string tgId to int64 // Convert string tgId to int64
if _, ok := c["tgId"]; ok { if _, ok := c["tgId"]; ok {
var tgId any = c["tgId"] tgId := c["tgId"]
if tgIdStr, ok2 := tgId.(string); ok2 { if tgIdStr, ok2 := tgId.(string); ok2 {
tgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, " ", ""), 10, 64) tgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, " ", ""), 10, 64)
if err == nil { if err == nil {
@@ -170,7 +171,7 @@ func (s *InboundService) MigrationRequirements() {
var count int64 var count int64
tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count) tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
if count == 0 { if count == 0 {
s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient) _ = s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient)
} }
} }
} }
@@ -212,7 +213,7 @@ func (s *InboundService) MigrationRequirements() {
for _, ep := range externalProxy { for _, ep := range externalProxy {
var reverses any var reverses any
var stream map[string]any var stream map[string]any
json.Unmarshal([]byte(ep.StreamSettings), &stream) _ = json.Unmarshal([]byte(ep.StreamSettings), &stream)
if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok { if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok {
if settings, ok := tlsSettings["settings"].(map[string]any); ok { if settings, ok := tlsSettings["settings"].(map[string]any); ok {
if domains, ok := settings["domains"].([]any); ok { if domains, ok := settings["domains"].([]any); ok {

View File

@@ -999,7 +999,7 @@ func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
db := database.GetDB() db := database.GetDB()
var rows []xray.ClientTraffic var rows []xray.ClientTraffic
err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
result := make(map[string]int64, len(rows)) result := make(map[string]int64, len(rows))
@@ -1029,7 +1029,7 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
clients := make([]xray.ClientTraffic, 0, len(uniqEmails)) clients := make([]xray.ClientTraffic, 0, len(uniqEmails))
for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) { for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) {
var page []xray.ClientTraffic var page []xray.ClientTraffic
if err := db.Where("email IN ?", batch).Find(&page).Error; err != nil && err != gorm.ErrRecordNotFound { if err := db.Where("email IN ?", batch).Find(&page).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, err return nil, nil, err
} }
clients = append(clients, page...) clients = append(clients, page...)

View File

@@ -3,6 +3,7 @@ package service
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -224,7 +225,7 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
} }
for inbound_index := range inbounds { for inbound_index := range inbounds {
settings := map[string]any{} settings := map[string]any{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) _ = json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]any) clients, ok := settings["clients"].([]any)
if ok { if ok {
var newClients []any var newClients []any
@@ -357,7 +358,7 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
} }
for inbound_index := range inbounds { for inbound_index := range inbounds {
settings := map[string]any{} settings := map[string]any{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) _ = json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, _ := settings["clients"].([]any) clients, _ := settings["clients"].([]any)
if len(clients) == 0 { if len(clients) == 0 {
continue continue
@@ -760,7 +761,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
continue continue
} }
if len(newClients) == 0 { if len(newClients) == 0 {
s.DelInbound(inbound.Id) _, _ = s.DelInbound(inbound.Id)
continue continue
} }
settings["clients"] = newClients settings["clients"] = newClients
@@ -827,7 +828,7 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
// Retrieve inbounds where settings contain the given tgId // Retrieve inbounds where settings contain the given tgId
err := db.Model(model.Inbound{}).Where("settings LIKE ?", fmt.Sprintf(`%%"tgId": %d%%`, tgId)).Find(&inbounds).Error err := db.Model(model.Inbound{}).Where("settings LIKE ?", fmt.Sprintf(`%%"tgId": %d%%`, tgId)).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err) logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err)
return nil, err return nil, err
} }
@@ -853,7 +854,7 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) { for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) {
var page []*xray.ClientTraffic var page []*xray.ClientTraffic
if err = db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil { if err = db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
if err == gorm.ErrRecordNotFound { if errors.Is(err, gorm.ErrRecordNotFound) {
continue continue
} }
logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", batch, err) logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", batch, err)
@@ -1008,7 +1009,7 @@ func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.Client
// Search for inbound settings that contain the query // Search for inbound settings that contain the query
err = db.Model(model.Inbound{}).Where("settings LIKE ?", "%\""+query+"\"%").First(inbound).Error err = db.Model(model.Inbound{}).Where("settings LIKE ?", "%\""+query+"\"%").First(inbound).Error
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warningf("Inbound settings containing query %s not found: %v", query, err) logger.Warningf("Inbound settings containing query %s not found: %v", query, err)
return nil, err return nil, err
} }
@@ -1041,7 +1042,7 @@ func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.Client
// Retrieve ClientTraffic based on the found email // Retrieve ClientTraffic based on the found email
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Warningf("ClientTraffic for email %s not found: %v", traffic.Email, err) logger.Warningf("ClientTraffic for email %s not found: %v", traffic.Email, err)
return nil, err return nil, err
} }

View File

@@ -49,7 +49,7 @@ func TestUpdateInbound_RegeneratesAutoTagOnPortChange(t *testing.T) {
// the returned object) which is what the save would use. // the returned object) which is what the save would use.
func TestUpdateInbound_NodeTagKeepsPrefixWhenNodeIdOmitted(t *testing.T) { func TestUpdateInbound_NodeTagKeepsPrefixWhenNodeIdOmitted(t *testing.T) {
setupConflictDB(t) setupConflictDB(t)
seedInboundConflictNode(t, "n1-in-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{"clients":[]}`, intPtr(1)) seedInboundConflictNode(t, "n1-in-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{"clients":[]}`, new(1))
var existing model.Inbound var existing model.Inbound
if err := database.GetDB().Where("tag = ?", "n1-in-443-tcp").First(&existing).Error; err != nil { if err := database.GetDB().Where("tag = ?", "n1-in-443-tcp").First(&existing).Error; err != nil {

View File

@@ -1,6 +1,7 @@
package integration package integration
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -21,7 +22,11 @@ var nordHTTPClient = &http.Client{Timeout: 15 * time.Second}
const maxResponseSize = 10 << 20 const maxResponseSize = 10 << 20
func (s *NordService) GetCountries() (string, error) { func (s *NordService) GetCountries() (string, error) {
resp, err := nordHTTPClient.Get("https://api.nordvpn.com/v1/countries") req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://api.nordvpn.com/v1/countries", nil)
if reqErr != nil {
return "", reqErr
}
resp, err := nordHTTPClient.Do(req)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -44,7 +49,11 @@ func (s *NordService) GetServers(countryId string) (string, error) {
} }
} }
url := fmt.Sprintf("https://api.nordvpn.com/v2/servers?limit=0&filters[servers_technologies][id]=35&filters[country_id]=%s", countryId) url := fmt.Sprintf("https://api.nordvpn.com/v2/servers?limit=0&filters[servers_technologies][id]=35&filters[country_id]=%s", countryId)
resp, err := nordHTTPClient.Get(url) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if reqErr != nil {
return "", reqErr
}
resp, err := nordHTTPClient.Do(req)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -89,7 +98,7 @@ func (s *NordService) SetKey(privateKey string) (string, error) {
"token": "", "token": "",
} }
data, _ := json.Marshal(nordData) data, _ := json.Marshal(nordData)
err := s.SettingService.SetNord(string(data)) err := s.SetNord(string(data))
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -98,7 +107,7 @@ func (s *NordService) SetKey(privateKey string) (string, error) {
func (s *NordService) GetCredentials(token string) (string, error) { func (s *NordService) GetCredentials(token string) (string, error) {
url := "https://api.nordvpn.com/v1/users/services/credentials" url := "https://api.nordvpn.com/v1/users/services/credentials"
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -134,7 +143,7 @@ func (s *NordService) GetCredentials(token string) (string, error) {
"token": token, "token": token,
} }
data, _ := json.Marshal(nordData) data, _ := json.Marshal(nordData)
err = s.SettingService.SetNord(string(data)) err = s.SetNord(string(data))
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -143,9 +152,9 @@ func (s *NordService) GetCredentials(token string) (string, error) {
} }
func (s *NordService) GetNordData() (string, error) { func (s *NordService) GetNordData() (string, error) {
return s.SettingService.GetNord() return s.GetNord()
} }
func (s *NordService) DelNordData() error { func (s *NordService) DelNordData() error {
return s.SettingService.SetNord("") return s.SetNord("")
} }

View File

@@ -2,6 +2,7 @@ package integration
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -27,11 +28,11 @@ const (
) )
func (s *WarpService) GetWarpData() (string, error) { func (s *WarpService) GetWarpData() (string, error) {
return s.SettingService.GetWarp() return s.GetWarp()
} }
func (s *WarpService) DelWarpData() error { func (s *WarpService) DelWarpData() error {
return s.SettingService.SetWarp("") return s.SetWarp("")
} }
func (s *WarpService) GetWarpConfig() (string, error) { func (s *WarpService) GetWarpConfig() (string, error) {
@@ -41,7 +42,7 @@ func (s *WarpService) GetWarpConfig() (string, error) {
} }
url := fmt.Sprintf("%s/reg/%s", warpAPIBase, warpData["device_id"]) url := fmt.Sprintf("%s/reg/%s", warpAPIBase, warpData["device_id"])
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -67,7 +68,7 @@ func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error
return "", err return "", err
} }
req, err := http.NewRequest(http.MethodPost, warpAPIBase+"/reg", bytes.NewReader(reqBody)) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, warpAPIBase+"/reg", bytes.NewReader(reqBody))
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -116,7 +117,7 @@ func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.SettingService.SetWarp(string(warpJSON)); err != nil { if err := s.SetWarp(string(warpJSON)); err != nil {
return "", err return "", err
} }
@@ -142,7 +143,7 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) {
return "", err return "", err
} }
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(reqBody)) req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, url, bytes.NewReader(reqBody))
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -167,7 +168,7 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.SettingService.SetWarp(string(newWarpData)); err != nil { if err := s.SetWarp(string(newWarpData)); err != nil {
return "", err return "", err
} }
return string(newWarpData), nil return string(newWarpData), nil
@@ -213,7 +214,7 @@ func (s *WarpService) ChangeWarpIP() (string, error) {
// loadWarpCreds reads the stored warp JSON and ensures access_token + device_id are set. // loadWarpCreds reads the stored warp JSON and ensures access_token + device_id are set.
func (s *WarpService) loadWarpCreds() (map[string]string, error) { func (s *WarpService) loadWarpCreds() (map[string]string, error) {
warp, err := s.SettingService.GetWarp() warp, err := s.GetWarp()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -832,7 +832,7 @@ func (s *NodeService) withOutboundBridge(nodeID int, outboundTag string, fn func
return return
} }
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0")
if err != nil { if err != nil {
fn("") fn("")
return return

View File

@@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
@@ -33,10 +34,12 @@ func (f *fakeNodeRuntime) UpdateUser(context.Context, *model.Inbound, string, mo
f.updateUser.Add(1) f.updateUser.Add(1)
return nil return nil
} }
func (f *fakeNodeRuntime) DeleteUser(context.Context, *model.Inbound, string) error { func (f *fakeNodeRuntime) DeleteUser(context.Context, *model.Inbound, string) error {
f.deleteUser.Add(1) f.deleteUser.Add(1)
return nil return nil
} }
func (f *fakeNodeRuntime) AddClient(context.Context, *model.Inbound, model.Client) error { func (f *fakeNodeRuntime) AddClient(context.Context, *model.Inbound, model.Client) error {
f.addClient.Add(1) f.addClient.Add(1)
return nil return nil

View File

@@ -5,11 +5,12 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gorm.io/gorm"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
"github.com/mhsanaei/3x-ui/v3/internal/xray" "github.com/mhsanaei/3x-ui/v3/internal/xray"
"gorm.io/gorm"
) )
func initTrafficTestDB(t *testing.T) *gorm.DB { func initTrafficTestDB(t *testing.T) *gorm.DB {

View File

@@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
) )

View File

@@ -1,6 +1,7 @@
package outbound package outbound
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
@@ -196,7 +197,7 @@ func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundRes
func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult { func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult {
r := TestEndpointResult{Address: endpoint} r := TestEndpointResult{Address: endpoint}
start := time.Now() start := time.Now()
conn, err := net.DialTimeout("tcp", endpoint, timeout) conn, err := (&net.Dialer{Timeout: timeout}).DialContext(context.Background(), "tcp", endpoint)
r.Delay = time.Since(start).Milliseconds() r.Delay = time.Since(start).Milliseconds()
if err != nil { if err != nil {
r.Error = err.Error() r.Error = err.Error()

View File

@@ -101,7 +101,7 @@ func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allO
func (s *OutboundService) TestOutbounds(outboundsJSON string, testURL string, allOutboundsJSON string, mode string) ([]*TestOutboundResult, error) { func (s *OutboundService) TestOutbounds(outboundsJSON string, testURL string, allOutboundsJSON string, mode string) ([]*TestOutboundResult, error) {
var raw []json.RawMessage var raw []json.RawMessage
if err := json.Unmarshal([]byte(outboundsJSON), &raw); err != nil { if err := json.Unmarshal([]byte(outboundsJSON), &raw); err != nil {
return nil, fmt.Errorf("invalid outbounds JSON: %v", err) return nil, fmt.Errorf("invalid outbounds JSON: %w", err)
} }
if len(raw) > maxBatchItems { if len(raw) > maxBatchItems {
return nil, fmt.Errorf("too many outbounds in one request (max %d)", maxBatchItems) return nil, fmt.Errorf("too many outbounds in one request (max %d)", maxBatchItems)
@@ -253,7 +253,7 @@ func (s *OutboundService) testOutboundsParsed(items []map[string]any, testURL st
func runHTTPProbeBatch(items []*httpBatchItem, allOutbounds []any, testURL string) (retryPerItem bool, err error) { func runHTTPProbeBatch(items []*httpBatchItem, allOutbounds []any, testURL string) (retryPerItem bool, err error) {
ports, release, err := reserveLoopbackPorts(len(items)) ports, release, err := reserveLoopbackPorts(len(items))
if err != nil { if err != nil {
return false, fmt.Errorf("Failed to reserve test ports: %v", err) return false, fmt.Errorf("Failed to reserve test ports: %w", err)
} }
defer release() defer release()
@@ -261,14 +261,14 @@ func runHTTPProbeBatch(items []*httpBatchItem, allOutbounds []any, testURL strin
configPath, err := createTestConfigPath() configPath, err := createTestConfigPath()
if err != nil { if err != nil {
return false, fmt.Errorf("Failed to create test config path: %v", err) return false, fmt.Errorf("Failed to create test config path: %w", err)
} }
defer os.Remove(configPath) defer os.Remove(configPath)
proc := newBatchProcess(cfg, configPath) proc := newBatchProcess(cfg, configPath)
defer func() { defer func() {
if proc.IsRunning() { if proc.IsRunning() {
proc.Stop() _ = proc.Stop()
} }
}() }()
@@ -279,9 +279,9 @@ func runHTTPProbeBatch(items []*httpBatchItem, allOutbounds []any, testURL strin
if err := proc.Start(); err != nil { if err := proc.Start(); err != nil {
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
// Binary missing — per-item retries would all fail the same way. // Binary missing — per-item retries would all fail the same way.
return false, fmt.Errorf("Failed to start test xray instance: %v", err) return false, fmt.Errorf("Failed to start test xray instance: %w", err)
} }
return true, fmt.Errorf("Failed to start test xray instance: %v", err) return true, fmt.Errorf("Failed to start test xray instance: %w", err)
} }
if err := waitForPortsReady(proc, ports, batchPortsReadyTimeout); err != nil { if err := waitForPortsReady(proc, ports, batchPortsReadyTimeout); err != nil {
@@ -330,7 +330,7 @@ func waitForPortsReady(proc batchProcess, ports []int, timeout time.Duration) *p
if !proc.IsRunning() { if !proc.IsRunning() {
return &portsReadyError{msg: "Xray process exited: " + proc.GetResult(), exited: true} return &portsReadyError{msg: "Xray process exited: " + proc.GetResult(), exited: true}
} }
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond) conn, err := (&net.Dialer{Timeout: 100 * time.Millisecond}).DialContext(context.Background(), "tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err == nil { if err == nil {
conn.Close() conn.Close()
break break
@@ -529,7 +529,7 @@ func reserveLoopbackPorts(n int) ([]int, func(), error) {
} }
ports := make([]int, 0, n) ports := make([]int, 0, n)
for range n { for range n {
l, err := net.Listen("tcp", "127.0.0.1:0") l, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0")
if err != nil { if err != nil {
release() release()
return nil, nil, err return nil, nil, err

View File

@@ -281,7 +281,7 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
return rejectPrivateHost(ctx, req.URL.Hostname()) return rejectPrivateHost(ctx, req.URL.Hostname())
} }
req, err := http.NewRequest("GET", sub.Url, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, sub.Url, nil)
if err != nil { if err != nil {
s.recordError(sub, err) s.recordError(sub, err)
return nil, err return nil, err
@@ -295,7 +295,7 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("http %d", resp.StatusCode) err := fmt.Errorf("http %d", resp.StatusCode)
s.recordError(sub, err) s.recordError(sub, err)
return nil, err return nil, err

View File

@@ -1,6 +1,7 @@
package panel package panel
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -157,7 +158,7 @@ func (s *PanelService) startUpdate(useDev bool) error {
if systemdRun, err := exec.LookPath("systemd-run"); err == nil { if systemdRun, err := exec.LookPath("systemd-run"); err == nil {
unitName := fmt.Sprintf("x-ui-web-update-%d", time.Now().Unix()) unitName := fmt.Sprintf("x-ui-web-update-%d", time.Now().Unix())
cmd := exec.Command(systemdRun, cmd := exec.CommandContext(context.Background(), systemdRun,
"--unit", unitName, "--unit", unitName,
"--setenv", "XUI_MAIN_FOLDER="+mainFolder, "--setenv", "XUI_MAIN_FOLDER="+mainFolder,
"--setenv", "XUI_SERVICE="+serviceFolder, "--setenv", "XUI_SERVICE="+serviceFolder,
@@ -179,7 +180,7 @@ func (s *PanelService) startUpdate(useDev bool) error {
} }
} }
cmd := exec.Command(bash, "-lc", updateScript) cmd := exec.CommandContext(context.Background(), bash, "-lc", updateScript)
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
"XUI_MAIN_FOLDER="+mainFolder, "XUI_MAIN_FOLDER="+mainFolder,
"XUI_SERVICE="+serviceFolder, "XUI_SERVICE="+serviceFolder,
@@ -199,7 +200,11 @@ func (s *PanelService) startUpdate(useDev bool) error {
func downloadPanelUpdater() (string, error) { func downloadPanelUpdater() (string, error) {
client := (&service.SettingService{}).NewProxiedHTTPClient(15 * time.Second) client := (&service.SettingService{}).NewProxiedHTTPClient(15 * time.Second)
resp, err := client.Get(panelUpdaterURL) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, panelUpdaterURL, nil)
if reqErr != nil {
return "", fmt.Errorf("download panel updater: %w", reqErr)
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("download panel updater: %w", err) return "", fmt.Errorf("download panel updater: %w", err)
} }
@@ -228,7 +233,7 @@ func downloadPanelUpdater() (string, error) {
if n > maxPanelUpdaterBytes { if n > maxPanelUpdaterBytes {
return "", fmt.Errorf("panel updater exceeds %d bytes", maxPanelUpdaterBytes) return "", fmt.Errorf("panel updater exceeds %d bytes", maxPanelUpdaterBytes)
} }
if err := file.Chmod(0700); err != nil { if err := file.Chmod(0o700); err != nil {
return "", err return "", err
} }
ok = true ok = true
@@ -254,7 +259,11 @@ func fetchPanelRelease(tag string) (*service.Release, error) {
url = "https://api.github.com/repos/MHSanaei/3x-ui/releases/tags/" + tag url = "https://api.github.com/repos/MHSanaei/3x-ui/releases/tags/" + tag
} }
client := (&service.SettingService{}).NewProxiedHTTPClient(10 * time.Second) client := (&service.SettingService{}).NewProxiedHTTPClient(10 * time.Second)
resp, err := client.Get(url) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if reqErr != nil {
return nil, reqErr
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -3,14 +3,15 @@ package panel
import ( import (
"errors" "errors"
"github.com/xlzd/gotp"
"gorm.io/gorm"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap" ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap"
"github.com/mhsanaei/3x-ui/v3/internal/web/service" "github.com/mhsanaei/3x-ui/v3/internal/web/service"
"github.com/xlzd/gotp"
"gorm.io/gorm"
) )
// UserService provides business logic for user management and authentication. // UserService provides business logic for user management and authentication.
@@ -43,7 +44,7 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
Where("username = ?", username). Where("username = ?", username).
First(user). First(user).
Error Error
if err == gorm.ErrRecordNotFound { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("invalid credentials") return nil, errors.New("invalid credentials")
} else if err != nil { } else if err != nil {
logger.Warning("check user err:", err) logger.Warning("check user err:", err)
@@ -89,7 +90,6 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
if twoFactorEnable { if twoFactorEnable {
twoFactorToken, err := s.settingService.GetTwoFactorToken() twoFactorToken, err := s.settingService.GetTwoFactorToken()
if err != nil { if err != nil {
logger.Warning("check two factor token err:", err) logger.Warning("check two factor token err:", err)
return nil, err return nil, err
@@ -114,7 +114,6 @@ func (s *UserService) BumpLoginEpoch() error {
func (s *UserService) UpdateUser(id int, username string, password string) error { func (s *UserService) UpdateUser(id int, username string, password string) error {
db := database.GetDB() db := database.GetDB()
hashedPassword, err := crypto.HashPasswordAsBcrypt(password) hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
if err != nil { if err != nil {
return err return err
} }
@@ -125,8 +124,8 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
} }
if twoFactorEnable { if twoFactorEnable {
s.settingService.SetTwoFactorEnable(false) _ = s.settingService.SetTwoFactorEnable(false)
s.settingService.SetTwoFactorToken("") _ = s.settingService.SetTwoFactorToken("")
} }
return db.Model(model.User{}). return db.Model(model.User{}).

View File

@@ -65,7 +65,7 @@ func (s *WebSocketService) readPump(client *websocket.Client, conn *ws.Conn) {
}() }()
conn.SetReadLimit(wsClientReadLimit) conn.SetReadLimit(wsClientReadLimit)
conn.SetReadDeadline(time.Now().Add(wsPongWait)) _ = conn.SetReadDeadline(time.Now().Add(wsPongWait))
conn.SetPongHandler(func(string) error { conn.SetPongHandler(func(string) error {
return conn.SetReadDeadline(time.Now().Add(wsPongWait)) return conn.SetReadDeadline(time.Now().Add(wsPongWait))
}) })
@@ -94,9 +94,9 @@ func (s *WebSocketService) writePump(client *websocket.Client, conn *ws.Conn) {
for { for {
select { select {
case msg, ok := <-client.Send: case msg, ok := <-client.Send:
conn.SetWriteDeadline(time.Now().Add(wsWriteWait)) _ = conn.SetWriteDeadline(time.Now().Add(wsWriteWait))
if !ok { if !ok {
conn.WriteMessage(ws.CloseMessage, []byte{}) _ = conn.WriteMessage(ws.CloseMessage, []byte{})
return return
} }
if err := conn.WriteMessage(ws.TextMessage, msg); err != nil { if err := conn.WriteMessage(ws.TextMessage, msg); err != nil {
@@ -105,7 +105,7 @@ func (s *WebSocketService) writePump(client *websocket.Client, conn *ws.Conn) {
} }
case <-ticker.C: case <-ticker.C:
conn.SetWriteDeadline(time.Now().Add(wsWriteWait)) _ = conn.SetWriteDeadline(time.Now().Add(wsWriteWait))
if err := conn.WriteMessage(ws.PingMessage, nil); err != nil { if err := conn.WriteMessage(ws.PingMessage, nil); err != nil {
logger.Debugf("WebSocket ping error for client %s: %v", client.ID, err) logger.Debugf("WebSocket ping error for client %s: %v", client.ID, err)
return return

View File

@@ -6,10 +6,11 @@ import (
"sync" "sync"
"testing" "testing"
"github.com/op/go-logging"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/op/go-logging"
) )
// the panel logger is a process-wide singleton. init it once per test // the panel logger is a process-wide singleton. init it once per test
@@ -57,9 +58,6 @@ func seedInboundConflictNode(t *testing.T, tag, listen string, port int, protoco
} }
} }
//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 {
name string name string
@@ -410,7 +408,7 @@ func TestResolveInboundTag_RespectsCallerTagWhenFree(t *testing.T) {
Port: 5000, Port: 5000,
Protocol: model.VLESS, Protocol: model.VLESS,
StreamSettings: `{"network":"tcp"}`, StreamSettings: `{"network":"tcp"}`,
NodeID: intPtr(1), NodeID: new(1),
} }
got, err := svc.resolveInboundTag(pushed, 0) got, err := svc.resolveInboundTag(pushed, 0)
if err != nil { if err != nil {
@@ -481,7 +479,7 @@ func TestGenerateInboundTag_NodePrefix(t *testing.T) {
Listen: "0.0.0.0", Listen: "0.0.0.0",
Port: 443, Port: 443,
Protocol: model.VLESS, Protocol: model.VLESS,
NodeID: intPtr(1), NodeID: new(1),
} }
got, err := svc.generateInboundTag(in, 0) got, err := svc.generateInboundTag(in, 0)
if err != nil { if err != nil {
@@ -503,7 +501,7 @@ func TestGenerateInboundTag_NodePrefixedDoesNotCollideWithLocal(t *testing.T) {
Listen: "0.0.0.0", Listen: "0.0.0.0",
Port: 443, Port: 443,
Protocol: model.VLESS, Protocol: model.VLESS,
NodeID: intPtr(1), NodeID: new(1),
} }
got, err := svc.generateInboundTag(in, 0) got, err := svc.generateInboundTag(in, 0)
if err != nil { if err != nil {
@@ -653,7 +651,7 @@ func TestIsAutoGeneratedTag(t *testing.T) {
{"canonical", "in-443-tcp", 443, nil, tcp, true}, {"canonical", "in-443-tcp", 443, nil, tcp, true},
{"canonical udp", "in-443-udp", 443, nil, transportUDP, true}, {"canonical udp", "in-443-udp", 443, nil, transportUDP, true},
{"dedup suffix", "in-443-tcp-2", 443, nil, tcp, true}, {"dedup suffix", "in-443-tcp-2", 443, nil, tcp, true},
{"node prefixed", "n1-in-443-tcp", 443, intPtr(1), tcp, true}, {"node prefixed", "n1-in-443-tcp", 443, new(1), tcp, true},
{"legacy listen-scoped is now custom", "in-127.0.0.1:443-tcp", 443, nil, tcp, false}, {"legacy listen-scoped is now custom", "in-127.0.0.1:443-tcp", 443, nil, tcp, false},
{"custom tag", "my-cool-tag", 443, nil, tcp, false}, {"custom tag", "my-cool-tag", 443, nil, tcp, false},
{"stale port", "in-443-tcp", 8443, nil, tcp, false}, {"stale port", "in-443-tcp", 8443, nil, tcp, false},
@@ -708,7 +706,7 @@ func TestCheckPortConflict_ReservedAPIPortAllowedOnNode(t *testing.T) {
Port: defaultXrayAPIPort, Port: defaultXrayAPIPort,
Protocol: model.VLESS, Protocol: model.VLESS,
StreamSettings: `{"network":"tcp"}`, StreamSettings: `{"network":"tcp"}`,
NodeID: intPtr(1), NodeID: new(1),
} }
if got, err := svc.checkPortConflict(candidate, 0); err != nil || got != nil { if got, err := svc.checkPortConflict(candidate, 0); err != nil || got != nil {
t.Fatalf("node inbound on the reserved API port must be allowed; got=%v err=%v", got, err) t.Fatalf("node inbound on the reserved API port must be allowed; got=%v err=%v", got, err)

View File

@@ -4,6 +4,7 @@ import (
"archive/zip" "archive/zip"
"bufio" "bufio"
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
@@ -233,7 +234,7 @@ func (s *ServerService) isFail2banInstalled() bool {
return s.fail2banInstalled return s.fail2banInstalled
} }
err := exec.Command("fail2ban-client", "-h").Run() err := exec.CommandContext(context.Background(), "fail2ban-client", "-h").Run()
s.fail2banInstalled = err == nil s.fail2banInstalled = err == nil
s.fail2banCheckedAt = time.Now() s.fail2banCheckedAt = time.Now()
return s.fail2banInstalled return s.fail2banInstalled
@@ -351,7 +352,11 @@ func getPublicIP(url string) string {
Timeout: 3 * time.Second, Timeout: 3 * time.Second,
} }
resp, err := client.Get(url) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if reqErr != nil {
return "N/A"
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return "N/A" return "N/A"
} }
@@ -772,7 +777,11 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
bufferSize = 8192 bufferSize = 8192
) )
resp, err := s.settingService.NewProxiedHTTPClient(10 * time.Second).Get(XrayURL) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, XrayURL, nil)
if reqErr != nil {
return nil, reqErr
}
resp, err := s.settingService.NewProxiedHTTPClient(10 * time.Second).Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -872,7 +881,11 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName) url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
client := s.settingService.NewProxiedHTTPClient(60 * time.Second) client := s.settingService.NewProxiedHTTPClient(60 * time.Second)
resp, err := client.Get(url) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if reqErr != nil {
return "", reqErr
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -934,7 +947,11 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
// fetchXrayDigestSHA256 downloads the .dgst sidecar XTLS publishes next to each // fetchXrayDigestSHA256 downloads the .dgst sidecar XTLS publishes next to each
// release asset and returns the SHA2-256 hex digest it lists. // release asset and returns the SHA2-256 hex digest it lists.
func (s *ServerService) fetchXrayDigestSHA256(client *http.Client, dgstURL string) (string, error) { func (s *ServerService) fetchXrayDigestSHA256(client *http.Client, dgstURL string) (string, error) {
resp, err := client.Get(dgstURL) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, dgstURL, nil)
if reqErr != nil {
return "", fmt.Errorf("download xray checksum: %w", reqErr)
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("download xray checksum: %w", err) return "", fmt.Errorf("download xray checksum: %w", err)
} }
@@ -1009,7 +1026,7 @@ func (s *ServerService) UpdateXray(version string) error {
return err return err
} }
defer zipFile.Close() defer zipFile.Close()
if err := os.MkdirAll(filepath.Dir(fileName), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(fileName), 0o755); err != nil {
return err return err
} }
tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*") tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*")
@@ -1031,7 +1048,7 @@ func (s *ServerService) UpdateXray(version string) error {
if n > maxXrayBinaryBytes { if n > maxXrayBinaryBytes {
return fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes) return fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes)
} }
if err := tmpFile.Chmod(0755); err != nil { if err := tmpFile.Chmod(0o755); err != nil {
return err return err
} }
if err := tmpFile.Close(); err != nil { if err := tmpFile.Close(); err != nil {
@@ -1099,7 +1116,7 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
} }
// Use hardcoded command with validated parameters // Use hardcoded command with validated parameters
cmd := exec.Command("journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level) cmd := exec.CommandContext(context.Background(), "journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
err = cmd.Run() err = cmd.Run()
@@ -1121,8 +1138,8 @@ func (s *ServerService) GetXrayLogs(
showBlocked string, showBlocked string,
showProxy string, showProxy string,
freedoms []string, freedoms []string,
blackholes []string) []LogEntry { blackholes []string,
) []LogEntry {
const ( const (
Direct = iota Direct = iota
Blocked Blocked
@@ -1149,12 +1166,12 @@ func (s *ServerService) GetXrayLogs(
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
if line == "" || strings.Contains(line, "api -> api") { if line == "" || strings.Contains(line, "api -> api") {
//skipping empty lines and api calls // skipping empty lines and api calls
continue continue
} }
if filter != "" && !strings.Contains(line, filter) { if filter != "" && !strings.Contains(line, filter) {
//applying filter if it's not empty // applying filter if it's not empty
continue continue
} }
@@ -1580,7 +1597,7 @@ func (s *ServerService) exportPostgresDB() ([]byte, error) {
if err != nil { if err != nil {
return nil, common.NewErrorf("invalid PostgreSQL DSN: %v", err) return nil, common.NewErrorf("invalid PostgreSQL DSN: %v", err)
} }
cmd := exec.Command(bin, "--format=custom", "--no-owner", "--no-privileges", "--dbname", dbname) cmd := exec.CommandContext(context.Background(), bin, "--format=custom", "--no-owner", "--no-privileges", "--dbname", dbname)
cmd.Env = env cmd.Env = env
var out, stderr bytes.Buffer var out, stderr bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
@@ -1642,7 +1659,7 @@ func (s *ServerService) importPostgresDB(file multipart.File) error {
logger.Warningf("Failed to close existing DB before restore: %v", errClose) logger.Warningf("Failed to close existing DB before restore: %v", errClose)
} }
cmd := exec.Command(bin, cmd := exec.CommandContext(context.Background(), bin,
"--clean", "--if-exists", "--no-owner", "--no-privileges", "--clean", "--if-exists", "--no-owner", "--no-privileges",
"--single-transaction", "--dbname", dbname, tempPath, "--single-transaction", "--dbname", dbname, tempPath,
) )
@@ -1721,7 +1738,7 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
downloadFile := func(url, destPath string) error { downloadFile := func(url, destPath string) error {
var req *http.Request var req *http.Request
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil { if err != nil {
return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err) return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
} }
@@ -1818,7 +1835,7 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
func (s *ServerService) GetNewX25519Cert() (any, error) { func (s *ServerService) GetNewX25519Cert() (any, error) {
// Run the command // Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519") cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "x25519")
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
err := cmd.Run() err := cmd.Run()
@@ -1844,7 +1861,7 @@ func (s *ServerService) GetNewX25519Cert() (any, error) {
func (s *ServerService) GetNewmldsa65() (any, error) { func (s *ServerService) GetNewmldsa65() (any, error) {
// Run the command // Run the command
cmd := exec.Command(xray.GetBinaryPath(), "mldsa65") cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "mldsa65")
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
err := cmd.Run() err := cmd.Run()
@@ -2048,7 +2065,7 @@ func (s *ServerService) GetRemoteCertHash(server string) ([]string, error) {
func (s *ServerService) GetNewEchCert(sni string) (any, error) { func (s *ServerService) GetNewEchCert(sni string) (any, error) {
// Run the command // Run the command
cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni) cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
err := cmd.Run() err := cmd.Run()
@@ -2071,7 +2088,7 @@ func (s *ServerService) GetNewEchCert(sni string) (any, error) {
} }
func (s *ServerService) GetNewVlessEnc() (any, error) { func (s *ServerService) GetNewVlessEnc() (any, error) {
cmd := exec.Command(xray.GetBinaryPath(), "vlessenc") cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "vlessenc")
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
@@ -2166,7 +2183,7 @@ func (s *ServerService) GetNewUUID() (map[string]string, error) {
func (s *ServerService) GetNewmlkem768() (any, error) { func (s *ServerService) GetNewmlkem768() (any, error) {
// Run the command // Run the command
cmd := exec.Command(xray.GetBinaryPath(), "mlkem768") cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "mlkem768")
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
err := cmd.Run() err := cmd.Run()

View File

@@ -14,6 +14,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/config"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"

View File

@@ -283,7 +283,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
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)
return err return err
} }
parsedAdminIds = append(parsedAdminIds, int64(id)) parsedAdminIds = append(parsedAdminIds, id)
} }
} }
tgBotMutex.Lock() tgBotMutex.Lock()
@@ -472,7 +472,7 @@ func StopBot() {
userStateMgr.reset() userStateMgr.reset()
if handler != nil { if handler != nil {
handler.Stop() _ = handler.Stop()
} }
if cancel != nil { if cancel != nil {

View File

@@ -74,13 +74,13 @@ func (t *Tgbot) BuildClientDraftMessage() string {
var b strings.Builder var b strings.Builder
b.WriteString("📝 *New client draft*\r\n") b.WriteString("📝 *New client draft*\r\n")
b.WriteString(fmt.Sprintf("📧 Email: `%s`\r\n", client_Email)) fmt.Fprintf(&b, "📧 Email: `%s`\r\n", client_Email)
b.WriteString(fmt.Sprintf("🔗 Attached: %s\r\n", attached)) fmt.Fprintf(&b, "🔗 Attached: %s\r\n", attached)
b.WriteString(fmt.Sprintf("📊 Traffic: %s\r\n", traffic)) fmt.Fprintf(&b, "📊 Traffic: %s\r\n", traffic)
b.WriteString(fmt.Sprintf("📅 Expire: %s\r\n", expiry)) fmt.Fprintf(&b, "📅 Expire: %s\r\n", expiry)
b.WriteString(fmt.Sprintf("🔢 IP limit: %s\r\n", ipLimit)) fmt.Fprintf(&b, "🔢 IP limit: %s\r\n", ipLimit)
b.WriteString(fmt.Sprintf("👤 TG user: %s\r\n", tgID)) fmt.Fprintf(&b, "👤 TG user: %s\r\n", tgID)
b.WriteString(fmt.Sprintf("💬 Comment: %s\r\n", comment)) fmt.Fprintf(&b, "💬 Comment: %s\r\n", comment)
return b.String() return b.String()
} }
@@ -216,7 +216,6 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
} }
subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID) subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID)
} else { } else {
subJsonURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID) subJsonURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
} }
@@ -258,7 +257,7 @@ func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
} }
// Try to fetch raw subscription links. Prefer plain text response. // Try to fetch raw subscription links. Prefer plain text response.
req, err := http.NewRequest("GET", subURL, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, subURL, nil)
if err != nil { if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return return
@@ -376,7 +375,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
// Also generate a few individual links' QRs (first up to 5) // Also generate a few individual links' QRs (first up to 5)
subPageURL := subURL subPageURL := subURL
req, err := http.NewRequest("GET", subPageURL, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, subPageURL, nil)
if err == nil { if err == nil {
req.Header.Set("Accept", "text/plain, */*;q=0.1") req.Header.Set("Accept", "text/plain, */*;q=0.1")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

View File

@@ -31,7 +31,7 @@ func (t *Tgbot) getInboundUsages() string {
clients, listErr := t.clientService.ListForInbound(nil, inbound.Id) clients, listErr := t.clientService.ListForInbound(nil, inbound.Id)
if listErr == nil { if listErr == nil {
info.WriteString(fmt.Sprintf("👥 Clients: %d\r\n", len(clients))) fmt.Fprintf(&info, "👥 Clients: %d\r\n", len(clients))
} }
if inbound.ExpiryTime == 0 { if inbound.ExpiryTime == 0 {
@@ -126,11 +126,9 @@ func (t *Tgbot) getInboundClientsFor(inboundID int, action string) (*telego.Inli
for _, client := range clients { for _, client := range clients {
buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery(action+" "+client.Email))) buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery(action+" "+client.Email)))
} }
} else { } else {
return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed")) return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed"))
} }
} }
cols := 0 cols := 0
if len(buttons) < 6 { if len(buttons) < 6 {
@@ -252,11 +250,9 @@ func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error)
for _, client := range clients { for _, client := range clients {
buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery("client_get_usage "+client.Email))) buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery("client_get_usage "+client.Email)))
} }
} else { } else {
return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed")) return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed"))
} }
} }
cols := 0 cols := 0
if len(buttons) < 6 { if len(buttons) < 6 {

View File

@@ -49,7 +49,7 @@ func (t *Tgbot) SendBackupToAdmins() {
return return
} }
for i, adminId := range adminIds { for i, adminId := range adminIds {
t.sendBackup(int64(adminId)) t.sendBackup(adminId)
// Add delay between sends to avoid Telegram rate limits // Add delay between sends to avoid Telegram rate limits
if i < len(adminIds)-1 { if i < len(adminIds)-1 {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
@@ -63,7 +63,7 @@ func (t *Tgbot) sendExhaustedToAdmins() {
return return
} }
for _, adminId := range adminIds { for _, adminId := range adminIds {
t.getExhausted(int64(adminId)) t.getExhausted(adminId)
} }
} }

View File

@@ -141,7 +141,6 @@ func (t *Tgbot) OnReceive() {
userStateMgr.clear(message.Chat.ID) userStateMgr.clear(message.Chat.ID)
t.addClient(message.Chat.ID, t.BuildClientDraftMessage()) t.addClient(message.Chat.ID, t.BuildClientDraftMessage())
} }
} else { } else {
if message.UsersShared != nil { if message.UsersShared != nil {
if checkAdmin(message.From.ID) { if checkAdmin(message.From.ID) {
@@ -167,7 +166,7 @@ func (t *Tgbot) OnReceive() {
return nil return nil
}, th.AnyMessage()) }, th.AnyMessage())
h.Start() _ = h.Start()
}() }()
} }
@@ -205,7 +204,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
if isAdmin { if isAdmin {
t.searchClient(chatId, commandArgs[0]) t.searchClient(chatId, commandArgs[0])
} else { } else {
t.getClientUsage(chatId, int64(message.From.ID), commandArgs[0]) t.getClientUsage(chatId, message.From.ID, commandArgs[0])
} }
} else { } else {
msg += t.I18nBot("tgbot.commands.usage") msg += t.I18nBot("tgbot.commands.usage")
@@ -595,12 +594,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if traffic.ExpiryTime > 0 { if traffic.ExpiryTime > 0 {
if traffic.ExpiryTime-time.Now().Unix()*1000 < 0 { if traffic.ExpiryTime-time.Now().Unix()*1000 < 0 {
date = -int64(days * 24 * 60 * 60000) date = -(days * 24 * 60 * 60000)
} else { } else {
date = traffic.ExpiryTime + int64(days*24*60*60000) date = traffic.ExpiryTime + days*24*60*60000
} }
} else { } else {
date = traffic.ExpiryTime - int64(days*24*60*60000) date = traffic.ExpiryTime - days*24*60*60000
} }
} }
@@ -685,12 +684,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
var date int64 var date int64
if client_ExpiryTime > 0 { if client_ExpiryTime > 0 {
if client_ExpiryTime-time.Now().Unix()*1000 < 0 { if client_ExpiryTime-time.Now().Unix()*1000 < 0 {
date = -int64(days * 24 * 60 * 60000) date = -(days * 24 * 60 * 60000)
} else { } else {
date = client_ExpiryTime + int64(days*24*60*60000) date = client_ExpiryTime + days*24*60*60000
} }
} else { } else {
date = client_ExpiryTime - int64(days*24*60*60000) date = client_ExpiryTime - days*24*60*60000
} }
client_ExpiryTime = date client_ExpiryTime = date
@@ -1111,7 +1110,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} }
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds)
} }
} }
} }
@@ -1458,7 +1456,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
case "get_sorted_traffic_usage_report": case "get_sorted_traffic_usage_report":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
emails, err := t.inboundService.GetAllEmails() emails, err := t.inboundService.GetAllEmails()
if err != nil { if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove())
return return

View File

@@ -143,7 +143,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
continue continue
} }
settings := map[string]any{} settings := map[string]any{}
json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
dbClients, listErr := s.inboundService.clientService.ListForInbound(nil, inbound.Id) dbClients, listErr := s.inboundService.clientService.ListForInbound(nil, inbound.Id)
if listErr != nil { if listErr != nil {
@@ -240,7 +240,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if len(inbound.StreamSettings) > 0 { if len(inbound.StreamSettings) > 0 {
// Unmarshal stream JSON // Unmarshal stream JSON
var stream map[string]any var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream) _ = json.Unmarshal([]byte(inbound.StreamSettings), &stream)
// Remove the "settings" field under "tlsSettings" and "realitySettings" // Remove the "settings" field under "tlsSettings" and "realitySettings"
tlsSettings, ok1 := stream["tlsSettings"].(map[string]any) tlsSettings, ok1 := stream["tlsSettings"].(map[string]any)
@@ -930,7 +930,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
logger.Info("Xray config changes applied through the core API, no restart needed") logger.Info("Xray config changes applied through the core API, no restart needed")
return nil return nil
} }
p.Stop() _ = p.Stop()
} }
p = xray.NewProcess(xrayConfig) p = xray.NewProcess(xrayConfig)

View File

@@ -29,7 +29,7 @@ func (s *XraySettingService) SaveXraySetting(newXraySettings string) error {
if hoisted, err := EnsureStatsRouting(newXraySettings); err == nil { if hoisted, err := EnsureStatsRouting(newXraySettings); err == nil {
newXraySettings = hoisted newXraySettings = hoisted
} }
return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings) return s.saveSetting("xrayTemplateConfig", newXraySettings)
} }
func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {

View File

@@ -309,10 +309,10 @@ func (s *Server) startTask(restartXray bool) {
} }
} }
// Check whether xray is running every second // Check whether xray is running every second
s.cron.AddJob(cadenceXrayRunning, job.NewCheckXrayRunningJob()) _, _ = s.cron.AddJob(cadenceXrayRunning, job.NewCheckXrayRunningJob())
// Check if xray needs to be restarted every 30 seconds // Check if xray needs to be restarted every 30 seconds
s.cron.AddFunc(cadenceXrayRestart, func() { _, _ = s.cron.AddFunc(cadenceXrayRestart, func() {
if s.xrayService.IsNeedRestartAndSetFalse() { if s.xrayService.IsNeedRestartAndSetFalse() {
err := s.xrayService.RestartXray(false) err := s.xrayService.RestartXray(false)
if err != nil { if err != nil {
@@ -323,37 +323,37 @@ func (s *Server) startTask(restartXray bool) {
go func() { go func() {
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
s.cron.AddJob(cadenceXrayTraffic, job.NewXrayTrafficJob()) _, _ = s.cron.AddJob(cadenceXrayTraffic, job.NewXrayTrafficJob())
}() }()
// Reconcile mtproto (mtg) sidecars and scrape their traffic // Reconcile mtproto (mtg) sidecars and scrape their traffic
mtJob := job.NewMtprotoJob() mtJob := job.NewMtprotoJob()
s.cron.AddJob(cadenceMtproto, mtJob) _, _ = s.cron.AddJob(cadenceMtproto, mtJob)
go mtJob.Run() go mtJob.Run()
// check client ips from log file every 10 sec // check client ips from log file every 10 sec
s.cron.AddJob(cadenceClientIPScan, job.NewCheckClientIpJob()) _, _ = s.cron.AddJob(cadenceClientIPScan, job.NewCheckClientIpJob())
s.cron.AddJob(cadenceNodeHeartbeat, job.NewNodeHeartbeatJob()) _, _ = s.cron.AddJob(cadenceNodeHeartbeat, job.NewNodeHeartbeatJob())
s.cron.AddJob(cadenceNodeTraffic, job.NewNodeTrafficSyncJob()) _, _ = s.cron.AddJob(cadenceNodeTraffic, job.NewNodeTrafficSyncJob())
// Outbound subscription auto-refresh (respects per-sub updateInterval) // Outbound subscription auto-refresh (respects per-sub updateInterval)
s.cron.AddJob(cadenceOutboundSub, job.NewOutboundSubscriptionJob()) _, _ = s.cron.AddJob(cadenceOutboundSub, job.NewOutboundSubscriptionJob())
// check client ips from log file every day // check client ips from log file every day
s.cron.AddJob("@daily", job.NewClearLogsJob()) _, _ = s.cron.AddJob("@daily", job.NewClearLogsJob())
s.cron.AddJob("@hourly", job.NewWarpIpJob()) _, _ = s.cron.AddJob("@hourly", job.NewWarpIpJob())
// Inbound traffic reset jobs // Inbound traffic reset jobs
// Run every hour // Run every hour
s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly")) _, _ = s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly"))
// Run once a day, midnight // Run once a day, midnight
s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("daily")) _, _ = s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("daily"))
// Run once a week, midnight between Sat/Sun // Run once a week, midnight between Sat/Sun
s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly")) _, _ = s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly"))
// Run once a month, midnight, first of month // Run once a month, midnight, first of month
s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly")) _, _ = s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly"))
// LDAP sync scheduling // LDAP sync scheduling
if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled { if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled {
@@ -363,7 +363,7 @@ func (s *Server) startTask(restartXray bool) {
} }
j := job.NewLdapSyncJob() j := job.NewLdapSyncJob()
// job has zero-value services with method receivers that read settings on demand // job has zero-value services with method receivers that read settings on demand
s.cron.AddJob(runtime, j) _, _ = s.cron.AddJob(runtime, j)
} }
// Telegram-botdependent jobs: periodic stats report + callback-hash cleanup. // Telegram-botdependent jobs: periodic stats report + callback-hash cleanup.
@@ -383,21 +383,21 @@ func (s *Server) startTask(restartXray bool) {
} }
// check for Telegram bot callback query hash storage reset // check for Telegram bot callback query hash storage reset
s.cron.AddJob(cadenceCheckHash, job.NewCheckHashStorageJob()) _, _ = s.cron.AddJob(cadenceCheckHash, job.NewCheckHashStorageJob())
} }
// CPU monitor publishes cpu.high events; register it whenever any notifier // CPU monitor publishes cpu.high events; register it whenever any notifier
// (Telegram or Email) wants them, independent of the Telegram bot being on. // (Telegram or Email) wants them, independent of the Telegram bot being on.
if s.cpuAlarmWanted() { if s.cpuAlarmWanted() {
s.cron.AddJob(cadenceCPUAlarm, job.NewCheckCpuJob()) _, _ = s.cron.AddJob(cadenceCPUAlarm, job.NewCheckCpuJob())
} }
// Memory monitor publishes memory.high events; register it whenever any notifier wants them. // Memory monitor publishes memory.high events; register it whenever any notifier wants them.
if s.memoryAlarmWanted() { if s.memoryAlarmWanted() {
s.cron.AddJob(cadenceMemoryAlarm, job.NewCheckMemJob()) _, _ = s.cron.AddJob(cadenceMemoryAlarm, job.NewCheckMemJob())
} }
if mins := sys.MemoryReleaseIntervalMinutes(); mins > 0 { if mins := sys.MemoryReleaseIntervalMinutes(); mins > 0 {
s.cron.AddJob(fmt.Sprintf("@every %dm", mins), job.NewMemoryReleaseJob()) _, _ = s.cron.AddJob(fmt.Sprintf("@every %dm", mins), job.NewMemoryReleaseJob())
go func() { go func() {
time.Sleep(time.Minute) time.Sleep(time.Minute)
job.NewMemoryReleaseJob().Run() job.NewMemoryReleaseJob().Run()
@@ -479,7 +479,7 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) {
// This is an anonymous function, no function name // This is an anonymous function, no function name
defer func() { defer func() {
if err != nil { if err != nil {
s.Stop() _ = s.Stop()
} }
}() }()
@@ -553,7 +553,7 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) {
} }
} }
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
listener, err := net.Listen("tcp", listenAddr) listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", listenAddr)
if err != nil { if err != nil {
return err return err
} }
@@ -593,7 +593,7 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) {
} }
go func() { go func() {
s.httpServer.Serve(listener) _ = s.httpServer.Serve(listener)
}() }()
// Create event bus before startTask so jobs can use it // Create event bus before startTask so jobs can use it
@@ -660,7 +660,7 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) {
isTgbotenabled, err := s.settingService.GetTgbotEnabled() isTgbotenabled, err := s.settingService.GetTgbotEnabled()
if (err == nil) && (isTgbotenabled) { if (err == nil) && (isTgbotenabled) {
tgBot := s.tgbotService.NewTgbot() tgBot := s.tgbotService.NewTgbot()
tgBot.Start(i18nFS) _ = tgBot.Start(i18nFS)
// Subscribe Telegram notifications for event bus // Subscribe Telegram notifications for event bus
s.bus.Subscribe("tg-notifier", s.tgbotService.HandleEvent) s.bus.Subscribe("tg-notifier", s.tgbotService.HandleEvent)
} }
@@ -681,7 +681,7 @@ func (s *Server) StopPanelOnly() error {
func (s *Server) stop(stopXray bool, stopTgBot bool) error { func (s *Server) stop(stopXray bool, stopTgBot bool) error {
s.cancel() s.cancel()
if stopXray { if stopXray {
s.xrayService.StopXray() _ = s.xrayService.StopXray()
mtproto.GetManager().StopAll() mtproto.GetManager().StopAll()
} }
if s.cron != nil { if s.cron != nil {

View File

@@ -7,8 +7,9 @@ import (
"testing" "testing"
"time" "time"
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/op/go-logging" "github.com/op/go-logging"
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {

View File

@@ -2,6 +2,7 @@ package xray
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -74,7 +75,7 @@ func GetAccessLogPath() (string, error) {
} }
jsonConfig := map[string]any{} jsonConfig := map[string]any{}
err = json.Unmarshal([]byte(config), &jsonConfig) err = json.Unmarshal(config, &jsonConfig)
if err != nil { if err != nil {
logger.Warningf("Failed to parse JSON configuration: %s", err) logger.Warningf("Failed to parse JSON configuration: %s", err)
return "", err return "", err
@@ -92,7 +93,7 @@ func GetAccessLogPath() (string, error) {
// stopProcess calls Stop on the given Process instance. // stopProcess calls Stop on the given Process instance.
func stopProcess(p *Process) { func stopProcess(p *Process) {
p.Stop() _ = p.Stop()
} }
// Process wraps an Xray process instance and provides management methods. // Process wraps an Xray process instance and provides management methods.
@@ -475,7 +476,7 @@ func (p *process) refreshAPIPort() {
// refreshVersion updates the version string by running the Xray binary with -version. // refreshVersion updates the version string by running the Xray binary with -version.
func (p *process) refreshVersion() { func (p *process) refreshVersion() {
cmd := exec.Command(GetBinaryPath(), "-version") cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "-version")
data, err := cmd.Output() data, err := cmd.Output()
if err != nil { if err != nil {
p.version = "Unknown" p.version = "Unknown"
@@ -521,7 +522,7 @@ func (p *process) Start() (err error) {
return common.NewErrorf("Failed to write configuration file: %v", err) return common.NewErrorf("Failed to write configuration file: %v", err)
} }
cmd := exec.Command(GetBinaryPath(), "-c", configPath) cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "-c", configPath)
cmd.Stdout = p.logWriter cmd.Stdout = p.logWriter
cmd.Stderr = p.logWriter cmd.Stderr = p.logWriter

View File

@@ -12,8 +12,9 @@ import (
"testing" "testing"
"time" "time"
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/op/go-logging" "github.com/op/go-logging"
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
) )
func TestWriteFileAtomicModeAndRenameFailure(t *testing.T) { func TestWriteFileAtomicModeAndRenameFailure(t *testing.T) {
@@ -203,7 +204,7 @@ func markProcessHelperReady(t *testing.T) {
if readyPath == "" { if readyPath == "" {
t.Fatal("XRAY_PROCESS_READY is not set") t.Fatal("XRAY_PROCESS_READY is not set")
} }
if err := os.WriteFile(readyPath, []byte("ready"), 0644); err != nil { if err := os.WriteFile(readyPath, []byte("ready"), 0o644); err != nil {
t.Fatalf("write helper ready file: %v", err) t.Fatalf("write helper ready file: %v", err)
} }
} }

View File

@@ -7,8 +7,9 @@ import (
"sync" "sync"
"unsafe" "unsafe"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
) )
var ( var (
@@ -36,7 +37,7 @@ func ensureKillOnExitJob() (windows.Handle, error) {
uint32(unsafe.Sizeof(info)), uint32(unsafe.Sizeof(info)),
) )
if err != nil { if err != nil {
windows.CloseHandle(h) _ = windows.CloseHandle(h)
killOnExitJobErr = err killOnExitJobErr = err
return return
} }
@@ -59,7 +60,7 @@ func attachChildLifetime(cmd *exec.Cmd) {
logger.Warning("xray: OpenProcess for job attach failed:", err) logger.Warning("xray: OpenProcess for job attach failed:", err)
return return
} }
defer windows.CloseHandle(h) defer func() { _ = windows.CloseHandle(h) }()
if err := windows.AssignProcessToJobObject(job, h); err != nil { if err := windows.AssignProcessToJobObject(job, h); err != nil {
logger.Warning("xray: AssignProcessToJobObject failed:", err) logger.Warning("xray: AssignProcessToJobObject failed:", err)
} }

View File

@@ -51,7 +51,7 @@ func runWebServer() {
log.Fatalf("Unknown log level: %v", config.GetLogLevel()) log.Fatalf("Unknown log level: %v", config.GetLogLevel())
} }
godotenv.Load() _ = godotenv.Load()
for _, line := range sys.ApplyMemoryTuning() { for _, line := range sys.ApplyMemoryTuning() {
logger.Info(line) logger.Info(line)
@@ -171,8 +171,8 @@ func runWebServer() {
tgbot.StopBot() tgbot.StopBot()
// ------------------------------------------------------------ // ------------------------------------------------------------
server.Stop() _ = server.Stop()
subServer.Stop() _ = subServer.Stop()
log.Println("Shutting down servers.") log.Println("Shutting down servers.")
return return
} }
@@ -350,7 +350,7 @@ func updateSetting(port int, username string, password string, webBasePath strin
if err != nil { if err != nil {
fmt.Println("Failed to reset two-factor authentication:", err) fmt.Println("Failed to reset two-factor authentication:", err)
} else { } else {
settingService.SetTwoFactorToken("") _ = settingService.SetTwoFactorToken("")
fmt.Println("Two-factor authentication reset successfully") fmt.Println("Two-factor authentication reset successfully")
} }
} }

View File

@@ -57,9 +57,7 @@ func walkPackages(requests []packageRequest) ([]Schema, []Alias, error) {
} }
overrides := req.Overrides[ts.Name.Name] overrides := req.Overrides[ts.Name.Name]
for _, fld := range strct.Fields.List { for _, fld := range strct.Fields.List {
for _, f := range buildFields(fld, overrides) { s.Fields = append(s.Fields, buildFields(fld, overrides)...)
s.Fields = append(s.Fields, f)
}
} }
schemas = append(schemas, s) schemas = append(schemas, s)
continue continue