diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f78709d89..076cbff48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,21 @@ jobs: go test -run '^$' -fuzz 'FuzzParseLink$' -fuzztime=30s ./internal/util/link/ 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: runs-on: ubuntu-latest steps: diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..3838df853 --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/internal/database/api_token_timestamp_test.go b/internal/database/api_token_timestamp_test.go index 5bfa471b6..6489f5cd8 100644 --- a/internal/database/api_token_timestamp_test.go +++ b/internal/database/api_token_timestamp_test.go @@ -3,10 +3,11 @@ package database import ( "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database/model" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" + + "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) func TestNormalizeApiTokenCreatedAtSeconds(t *testing.T) { diff --git a/internal/database/db.go b/internal/database/db.go index 92b6b405c..79173da6d 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -4,6 +4,7 @@ package database import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -43,7 +44,7 @@ func IsPostgres() bool { if db == nil { 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. @@ -51,7 +52,7 @@ func Dialect() string { if db == nil { return "" } - return db.Dialector.Name() + return db.Name() } const ( @@ -363,7 +364,6 @@ func initUser() error { } if empty { hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword) - if err != nil { log.Printf("Error hashing default password: %v", err) return err @@ -580,7 +580,7 @@ func fail2banCanEnforce() bool { if runtime.GOOS == "windows" { 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 @@ -1038,7 +1038,7 @@ func InitDB(dbPath string) error { } default: dir := path.Dir(dbPath) - if err = os.MkdirAll(dir, 0755); err != nil { + if err = os.MkdirAll(dir, 0o755); err != nil { return err } // 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", } for _, p := range pragmas { - if _, err := sqlDB.Exec(p); err != nil { + if _, err := sqlDB.ExecContext(context.Background(), p); err != nil { return err } } diff --git a/internal/database/dump_sqlite.go b/internal/database/dump_sqlite.go index 8b71b48dd..c37839392 100644 --- a/internal/database/dump_sqlite.go +++ b/internal/database/dump_sqlite.go @@ -1,6 +1,7 @@ package database import ( + "context" "database/sql" "fmt" "os" @@ -50,23 +51,21 @@ func DumpSQLiteToBytes(srcPath string) ([]byte, error) { // Tables in creation order, each followed by its data. type object struct{ name, ddl string } 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 { return nil, err } + defer rows.Close() for rows.Next() { var o object if err := rows.Scan(&o.name, &o.ddl); err != nil { - rows.Close() return nil, err } tables = append(tables, o) } if err := rows.Err(); err != nil { - rows.Close() return nil, err } - rows.Close() for _, t := range tables { b.WriteString(t.ddl) @@ -85,24 +84,22 @@ func DumpSQLiteToBytes(srcPath string) ([]byte, error) { } // 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 { return nil, err } + defer rows2.Close() for rows2.Next() { var ddl string if err := rows2.Scan(&ddl); err != nil { - rows2.Close() return nil, err } b.WriteString(ddl) b.WriteString(";\n") } if err := rows2.Err(); err != nil { - rows2.Close() return nil, err } - rows2.Close() 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. - if _, err := sqlDB.Exec(string(script)); err != nil { + if _, err := sqlDB.ExecContext(context.Background(), string(script)); err != nil { sqlDB.Close() os.Remove(dstPath) 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. 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 { return err } @@ -213,6 +210,6 @@ func quoteSQLiteText(s string) string { func sqliteTableExists(db *sql.DB, name string) bool { 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 } diff --git a/internal/database/migrate_data.go b/internal/database/migrate_data.go index 6bf8d7e60..be00ee8d4 100644 --- a/internal/database/migrate_data.go +++ b/internal/database/migrate_data.go @@ -67,7 +67,7 @@ func MigrateData(srcPath, dstDSN string) error { 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 } @@ -144,7 +144,7 @@ func ExportPostgresToSQLite(srcDSN, dstPath string) error { if srcDSN == "" { 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 } // Start from an empty file so AutoMigrate creates the canonical schema. diff --git a/internal/eventbus/bus_test.go b/internal/eventbus/bus_test.go index f031000c6..43ae785d5 100644 --- a/internal/eventbus/bus_test.go +++ b/internal/eventbus/bus_test.go @@ -6,8 +6,9 @@ import ( "testing" "time" - "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/op/go-logging" + + "github.com/mhsanaei/3x-ui/v3/internal/logger" ) func TestMain(m *testing.M) { diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 00f8b503c..38be9a2ff 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -10,9 +10,10 @@ import ( "sync" "time" - "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/op/go-logging" + "github.com/mhsanaei/3x-ui/v3/internal/config" + "gopkg.in/natefinch/lumberjack.v2" ) diff --git a/internal/mtproto/manager.go b/internal/mtproto/manager.go index bbe7a4559..90c7abf2c 100644 --- a/internal/mtproto/manager.go +++ b/internal/mtproto/manager.go @@ -2,6 +2,7 @@ package mtproto import ( "bufio" + "context" "encoding/json" "fmt" "net" @@ -181,7 +182,7 @@ func (m *Manager) ensureLocked(inst Instance) error { cur.tag = inst.Tag return nil } - cur.proc.Stop() + _ = cur.proc.Stop() delete(m.procs, inst.Id) } metricsPort, err := FreeLocalPort() @@ -211,7 +212,7 @@ func (m *Manager) Remove(id int) { m.mu.Lock() defer m.mu.Unlock() if cur, ok := m.procs[id]; ok { - cur.proc.Stop() + _ = cur.proc.Stop() delete(m.procs, id) _ = os.Remove(configPathForID(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 { if _, ok := want[id]; !ok { - cur.proc.Stop() + _ = cur.proc.Stop() delete(m.procs, 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 // bridge port persisted into mtproto inbound settings. 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 { 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. func scrapeTraffic(port int) (up int64, down int64, ok bool) { 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 { 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) { labels = map[string]string{} - rest := line + var rest string if brace := strings.IndexByte(line, '{'); brace >= 0 { name = line[:brace] end := strings.IndexByte(line, '}') diff --git a/internal/mtproto/process.go b/internal/mtproto/process.go index 808dc47f4..2ab2c699b 100644 --- a/internal/mtproto/process.go +++ b/internal/mtproto/process.go @@ -5,6 +5,7 @@ package mtproto import ( + "context" "errors" "fmt" "os" @@ -154,7 +155,7 @@ func (p *Process) Start() error { if p.IsRunning() { 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.Stderr = p.logWriter p.cmd = cmd diff --git a/internal/mtproto/process_windows.go b/internal/mtproto/process_windows.go index 4ca1d4cdf..e87b62c33 100644 --- a/internal/mtproto/process_windows.go +++ b/internal/mtproto/process_windows.go @@ -7,8 +7,9 @@ import ( "sync" "unsafe" - "github.com/mhsanaei/3x-ui/v3/internal/logger" "golang.org/x/sys/windows" + + "github.com/mhsanaei/3x-ui/v3/internal/logger" ) var ( @@ -36,7 +37,7 @@ func ensureKillOnExitJob() (windows.Handle, error) { uint32(unsafe.Sizeof(info)), ) if err != nil { - windows.CloseHandle(h) + _ = windows.CloseHandle(h) killOnExitJobErr = err return } @@ -59,7 +60,7 @@ func attachChildLifetime(cmd *exec.Cmd) { logger.Warningf("mtproto: OpenProcess for job attach failed: %v", err) return } - defer windows.CloseHandle(h) + defer func() { _ = windows.CloseHandle(h) }() if err := windows.AssignProcessToJobObject(job, h); err != nil { logger.Warningf("mtproto: AssignProcessToJobObject failed: %v", err) } diff --git a/internal/sub/clash_service.go b/internal/sub/clash_service.go index 13d1811a7..c3aed8f45 100644 --- a/internal/sub/clash_service.go +++ b/internal/sub/clash_service.go @@ -241,7 +241,7 @@ func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound, proxy["type"] = "vless" proxy["uuid"] = client.ID var inboundSettings map[string]any - json.Unmarshal([]byte(inbound.Settings), &inboundSettings) + _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings) streamSecurity, _ := stream["security"].(string) if client.Flow != "" && vlessFlowAllowed(network, streamSecurity, inboundSettings) { proxy["flow"] = client.Flow @@ -259,7 +259,7 @@ func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound, proxy["type"] = "ss" proxy["password"] = client.Password var inboundSettings map[string]any - json.Unmarshal([]byte(inbound.Settings), &inboundSettings) + _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings) method, _ := inboundSettings["method"].(string) if method == "" { 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 { var streamSettings map[string]any - json.Unmarshal([]byte(stream), &streamSettings) + _ = json.Unmarshal([]byte(stream), &streamSettings) security, _ := streamSettings["security"].(string) switch security { case "tls": diff --git a/internal/sub/controller.go b/internal/sub/controller.go index ae5b57a88..04b97e315 100644 --- a/internal/sub/controller.go +++ b/internal/sub/controller.go @@ -16,6 +16,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/mhsanaei/3x-ui/v3/internal/logger" "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("Profile-Update-Interval", updateInterval) - //Basics + // Basics if 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))) } - //Advanced (Happ) + // Advanced (Happ) c.Writer.Header().Set("Routing-Enable", strconv.FormatBool(profileEnableRouting)) if profileRoutingRules != "" { c.Writer.Header().Set("Routing", profileRoutingRules) diff --git a/internal/sub/external_subscription.go b/internal/sub/external_subscription.go index 71a3fabf9..b187c6284 100644 --- a/internal/sub/external_subscription.go +++ b/internal/sub/external_subscription.go @@ -1,6 +1,7 @@ package sub import ( + "context" "encoding/base64" "io" "net/http" @@ -64,7 +65,7 @@ func fetchSubscriptionLinks(rawURL string) []string { } 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 { return nil, err } diff --git a/internal/sub/external_subscription_test.go b/internal/sub/external_subscription_test.go index 60af51987..0d1bca820 100644 --- a/internal/sub/external_subscription_test.go +++ b/internal/sub/external_subscription_test.go @@ -1,6 +1,7 @@ package sub import ( + "errors" "net/http" "net/http/httptest" "strings" @@ -14,7 +15,7 @@ func TestDoFetchSubscriptionLinks_RejectsOversizedBody(t *testing.T) { defer srv.Close() links, err := doFetchSubscriptionLinks(srv.URL) - if err != errSubscriptionBodyTooLarge { + if !errors.Is(err, errSubscriptionBodyTooLarge) { t.Fatalf("err = %v, want errSubscriptionBodyTooLarge", err) } if links != nil { diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index c3cf83b5f..febdfc187 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -29,7 +29,7 @@ type SubJsonService struct { func NewSubJsonService(mux string, rules string, finalMask string, subService *SubService) *SubJsonService { var configJson map[string]any var defaultOutbounds []json_util.RawMessage - json.Unmarshal([]byte(defaultJson), &configJson) + _ = json.Unmarshal([]byte(defaultJson), &configJson) if outboundSlices, ok := configJson["outbounds"].([]any); ok { for _, defaultOutbound := range outboundSlices { jsonBytes, _ := json.Marshal(defaultOutbound) @@ -41,7 +41,7 @@ func NewSubJsonService(mux string, rules string, finalMask string, subService *S var newRules []any routing, _ := configJson["routing"].(map[string]any) defaultRules, _ := routing["rules"].([]any) - json.Unmarshal([]byte(rules), &newRules) + _ = json.Unmarshal([]byte(rules), &newRules) defaultRules = append(newRules, defaultRules...) routing["rules"] = defaultRules 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 { var streamSettings map[string]any - json.Unmarshal([]byte(stream), &streamSettings) + _ = json.Unmarshal([]byte(stream), &streamSettings) security, _ := streamSettings["security"].(string) switch security { case "tls": @@ -392,7 +392,7 @@ func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_ut // Add encryption for VLESS outbound from inbound settings var inboundSettings map[string]any - json.Unmarshal([]byte(inbound.Settings), &inboundSettings) + _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings) encryption, _ := inboundSettings["encryption"].(string) settings := map[string]any{ @@ -423,7 +423,7 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u if inbound.Protocol == model.Shadowsocks { var inboundSettings map[string]any - json.Unmarshal([]byte(inbound.Settings), &inboundSettings) + _ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings) method, _ := inboundSettings["method"].(string) 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 - json.Unmarshal([]byte(inbound.Settings), &settings) + _ = json.Unmarshal([]byte(inbound.Settings), &settings) version, _ := settings["version"].(float64) outbound.Settings = map[string]any{ "version": int(version), @@ -482,7 +482,7 @@ func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, "port": inbound.Port, } - json.Unmarshal([]byte(inbound.StreamSettings), &stream) + _ = json.Unmarshal([]byte(inbound.StreamSettings), &stream) hyStream := stream["hysteriaSettings"].(map[string]any) outHyStream := map[string]any{ "version": int(version), diff --git a/internal/sub/service.go b/internal/sub/service.go index d93d4c273..6e39b70a4 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -453,12 +453,12 @@ func (s *SubService) projectThroughFallbackMaster(inbound *model.Inbound) bool { // + ws/grpc/etc. settings) stays the child's. func mergeStreamFromMaster(childStream, masterStream string) string { var stream map[string]any - json.Unmarshal([]byte(childStream), &stream) + _ = json.Unmarshal([]byte(childStream), &stream) if stream == nil { stream = map[string]any{} } var mst map[string]any - json.Unmarshal([]byte(masterStream), &mst) + _ = json.Unmarshal([]byte(masterStream), &mst) if mst == nil { return childStream } @@ -511,7 +511,7 @@ func (s *SubService) genMtprotoLink(inbound *model.Inbound, _ string) string { return "" } settings := map[string]any{} - json.Unmarshal([]byte(inbound.Settings), &settings) + _ = json.Unmarshal([]byte(inbound.Settings), &settings) secret, _ := settings["secret"].(string) if secret == "" { 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 var settings map[string]any - json.Unmarshal([]byte(inbound.Settings), &settings) + _ = json.Unmarshal([]byte(inbound.Settings), &settings) if encryption, ok := settings["encryption"].(string); ok { params["encryption"] = encryption } @@ -739,7 +739,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st clients, _ := s.inboundService.GetClients(inbound) var settings map[string]any - json.Unmarshal([]byte(inbound.Settings), &settings) + _ = json.Unmarshal([]byte(inbound.Settings), &settings) inboundPassword := settings["password"].(string) method := settings["method"].(string) clientIndex := findClientIndex(clients, email) @@ -808,7 +808,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin return "" } var stream map[string]any - json.Unmarshal([]byte(inbound.StreamSettings), &stream) + _ = json.Unmarshal([]byte(inbound.StreamSettings), &stream) clients, _ := s.inboundService.GetClients(inbound) clientIndex := -1 for i, client := range clients { @@ -877,7 +877,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin } var settings map[string]any - json.Unmarshal([]byte(inbound.Settings), &settings) + _ = json.Unmarshal([]byte(inbound.Settings), &settings) version, _ := settings["version"].(float64) protocol := "hysteria2" if int(version) == 1 { @@ -1008,7 +1008,7 @@ func findClientIndex(clients []model.Client, email string) int { func unmarshalStreamSettings(streamSettings string) map[string]any { var stream map[string]any - json.Unmarshal([]byte(streamSettings), &stream) + _ = json.Unmarshal([]byte(streamSettings), &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 { newObj := map[string]any{} 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 } } diff --git a/internal/sub/sub.go b/internal/sub/sub.go index c089de61f..858af331b 100644 --- a/internal/sub/sub.go +++ b/internal/sub/sub.go @@ -253,7 +253,7 @@ func (s *Server) Start() (err error) { // This is an anonymous function, no function name defer func() { if err != nil { - s.Stop() + _ = s.Stop() } }() @@ -288,7 +288,7 @@ func (s *Server) Start() (err error) { } 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 { return err } @@ -323,7 +323,7 @@ func (s *Server) Start() (err error) { } go func() { - s.httpServer.Serve(listener) + _ = s.httpServer.Serve(listener) }() return nil diff --git a/internal/tunnelmonitor/monitor.go b/internal/tunnelmonitor/monitor.go index 2c868efd4..ac10ba1ba 100644 --- a/internal/tunnelmonitor/monitor.go +++ b/internal/tunnelmonitor/monitor.go @@ -189,7 +189,7 @@ func (m *Monitor) Step(ctx context.Context) (bool, error) { } 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 diff --git a/internal/tunnelmonitor/monitor_test.go b/internal/tunnelmonitor/monitor_test.go index 728f25516..58dd8bfdb 100644 --- a/internal/tunnelmonitor/monitor_test.go +++ b/internal/tunnelmonitor/monitor_test.go @@ -9,8 +9,9 @@ import ( "testing" "time" - "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/op/go-logging" + + "github.com/mhsanaei/3x-ui/v3/internal/logger" ) func TestMain(m *testing.M) { diff --git a/internal/util/common/gorecover_test.go b/internal/util/common/gorecover_test.go index d9d0f3b04..29d31a316 100644 --- a/internal/util/common/gorecover_test.go +++ b/internal/util/common/gorecover_test.go @@ -5,8 +5,9 @@ import ( "testing" "time" - "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/op/go-logging" + + "github.com/mhsanaei/3x-ui/v3/internal/logger" ) func TestMain(m *testing.M) { diff --git a/internal/util/random/random_test.go b/internal/util/random/random_test.go index 57eb3c59d..bf714809c 100644 --- a/internal/util/random/random_test.go +++ b/internal/util/random/random_test.go @@ -15,7 +15,7 @@ func TestSeq_LengthAndAlphabet(t *testing.T) { isDigit := r >= '0' && r <= '9' isLower := 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) } } diff --git a/internal/util/sys/sys_windows.go b/internal/util/sys/sys_windows.go index 774f9c026..a6a39ef2f 100644 --- a/internal/util/sys/sys_windows.go +++ b/internal/util/sys/sys_windows.go @@ -66,7 +66,8 @@ func CPUPercentRaw() (float64, error) { uintptr(unsafe.Pointer(&userFT)), ) 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, errors.New("GetSystemTimes failed") diff --git a/internal/web/controller/api.go b/internal/web/controller/api.go index 553fb53fe..a4b4a83b4 100644 --- a/internal/web/controller/api.go +++ b/internal/web/controller/api.go @@ -5,7 +5,6 @@ import ( "strings" "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/tgbot" "github.com/mhsanaei/3x-ui/v3/internal/web/session" @@ -22,7 +21,6 @@ type APIController struct { hostController *HostController settingController *SettingController xraySettingController *XraySettingController - settingService service.SettingService userService panel.UserService apiTokenService panel.ApiTokenService Tgbot tgbot.Tgbot diff --git a/internal/web/controller/inbound.go b/internal/web/controller/inbound.go index fe95b075c..d641dea00 100644 --- a/internal/web/controller/inbound.go +++ b/internal/web/controller/inbound.go @@ -61,7 +61,6 @@ func (a *InboundController) broadcastInboundsUpdate(userId int) { // initRouter initializes the routes for inbound-related operations. func (a *InboundController) initRouter(g *gin.RouterGroup) { - g.GET("/list", a.getInbounds) g.GET("/list/slim", a.getInboundsSlim) g.GET("/options", a.getInboundOptions) diff --git a/internal/web/controller/server.go b/internal/web/controller/server.go index fae1db3b8..c57c95c9e 100644 --- a/internal/web/controller/server.go +++ b/internal/web/controller/server.go @@ -42,7 +42,6 @@ func NewServerController(g *gin.RouterGroup) *ServerController { // initRouter sets up the routes for server status, Xray management, and utility endpoints. func (a *ServerController) initRouter(g *gin.RouterGroup) { - g.GET("/status", a.status) g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) 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). func (a *ServerController) startTask() { c := global.GetWebServer().GetCron() - c.AddFunc("@every 2s", func() { + _, _ = c.AddFunc("@every 2s", func() { status := a.serverService.RefreshStatus() if status == nil { return @@ -97,7 +96,7 @@ func (a *ServerController) startTask() { a.xrayMetricsService.Sample(time.Now()) websocket.BroadcastStatus(status) }) - c.AddFunc("@every 1m", func() { + _, _ = c.AddFunc("@every 1m", func() { if err := service.PersistSystemMetrics(); err != nil { 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) if !filenameRegex.MatchString(filename) { - c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename")) + _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename")) return } c.Header("Content-Type", "application/octet-stream") 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 @@ -345,13 +344,13 @@ func (a *ServerController) getMigration(c *gin.Context) { return } if !filenameRegex.MatchString(filename) { - c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename")) + _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename")) return } c.Header("Content-Type", "application/octet-stream") 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. diff --git a/internal/web/job/check_client_ip_job.go b/internal/web/job/check_client_ip_job.go index 1a033c490..a28bae94c 100644 --- a/internal/web/job/check_client_ip_job.go +++ b/internal/web/job/check_client_ip_job.go @@ -1,6 +1,7 @@ package job import ( + "context" "encoding/json" "errors" "log" @@ -127,7 +128,7 @@ func (j *CheckClientIpJob) hasLimitIp() bool { } settings := map[string][]model.Client{} - json.Unmarshal([]byte(inbound.Settings), &settings) + _ = json.Unmarshal([]byte(inbound.Settings), &settings) clients := settings["clients"] for _, client := range clients { @@ -189,7 +190,7 @@ func (j *CheckClientIpJob) processObserved(observed map[string]map[string]int64, clientIpsRecord, err := j.getInboundClientIps(email) if err != nil { - j.addInboundClientIps(email, ipsWithTime) + _ = j.addInboundClientIps(email, ipsWithTime) continue } @@ -277,7 +278,7 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() bool { cmd := "fail2ban-client" args := []string{"-h"} - err := exec.Command(cmd, args...).Run() + err := exec.CommandContext(context.Background(), cmd, args...).Run() return err == nil } @@ -345,7 +346,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun } settings := map[string][]model.Client{} - json.Unmarshal([]byte(inbound.Settings), &settings) + _ = json.Unmarshal([]byte(inbound.Settings), &settings) clients := settings["clients"] // Find the client's IP limit @@ -372,7 +373,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun // Parse old IPs from database var oldIpsWithTime []IPWithTimestamp if inboundClientIps.Ips != "" { - json.Unmarshal([]byte(inboundClientIps.Ips), &oldIpsWithTime) + _ = json.Unmarshal([]byte(inboundClientIps.Ips), &oldIpsWithTime) } ipMap := mergeClientIps(oldIpsWithTime, newIpsWithTime, time.Now().Unix()-ipStaleAfterSeconds, observedAreLive) @@ -393,7 +394,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun if len(bannedLive) > 0 { 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 { logger.Errorf("failed to open IP limit log file: %s", err) return false @@ -455,7 +456,7 @@ func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, c if client.Email == clientEmail { // Convert client to map for API clientBytes, _ := json.Marshal(client) - json.Unmarshal(clientBytes, &clientConfig) + _ = json.Unmarshal(clientBytes, &clientConfig) break } } diff --git a/internal/web/job/check_client_ip_job_integration_test.go b/internal/web/job/check_client_ip_job_integration_test.go index 3575a66e7..ffa27ea04 100644 --- a/internal/web/job/check_client_ip_job_integration_test.go +++ b/internal/web/job/check_client_ip_job_integration_test.go @@ -9,10 +9,11 @@ import ( "testing" "time" + "github.com/op/go-logging" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" 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 diff --git a/internal/web/job/check_cpu_usage.go b/internal/web/job/check_cpu_usage.go index b8ad68f2b..6e11838c9 100644 --- a/internal/web/job/check_cpu_usage.go +++ b/internal/web/job/check_cpu_usage.go @@ -4,15 +4,12 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/eventbus" - "github.com/mhsanaei/3x-ui/v3/internal/web/service" "github.com/shirou/gopsutil/v4/cpu" ) // CheckCpuJob monitors CPU usage and publishes events when threshold is exceeded. -type CheckCpuJob struct { - settingService service.SettingService -} +type CheckCpuJob struct{} // NewCheckCpuJob creates a new CPU monitoring job instance. func NewCheckCpuJob() *CheckCpuJob { diff --git a/internal/web/job/check_memory_usage.go b/internal/web/job/check_memory_usage.go index 18dfd3a7a..63b20d604 100644 --- a/internal/web/job/check_memory_usage.go +++ b/internal/web/job/check_memory_usage.go @@ -2,15 +2,12 @@ package job import ( "github.com/mhsanaei/3x-ui/v3/internal/eventbus" - "github.com/mhsanaei/3x-ui/v3/internal/web/service" "github.com/shirou/gopsutil/v4/mem" ) // CheckMemJob monitors memory usage and publishes events when threshold is exceeded. -type CheckMemJob struct { - settingService service.SettingService -} +type CheckMemJob struct{} // NewCheckMemJob creates a new memory monitoring job instance. func NewCheckMemJob() *CheckMemJob { diff --git a/internal/web/job/clear_logs_job.go b/internal/web/job/clear_logs_job.go index f3dd8aec3..426d6a965 100644 --- a/internal/web/job/clear_logs_job.go +++ b/internal/web/job/clear_logs_job.go @@ -20,11 +20,11 @@ func NewClearLogsJob() *ClearLogsJob { // ensureFileExists creates the necessary directories and file if they don't exist func ensureFileExists(path string) error { dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0o755); err != nil { 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 { return err } @@ -48,13 +48,13 @@ func (j *ClearLogsJob) Run() { for i := range len(logFiles) { if i > 0 { // 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 { logger.Warning("Failed to open previous log file for writing:", logFilesPrev[i-1], "-", err) continue } - logFile, err := os.OpenFile(logFiles[i], os.O_RDONLY, 0644) + logFile, err := os.OpenFile(logFiles[i], os.O_RDONLY, 0o644) if err != nil { logger.Warning("Failed to open current log file for reading:", logFiles[i], "-", err) logFilePrev.Close() diff --git a/internal/web/job/clear_logs_job_test.go b/internal/web/job/clear_logs_job_test.go index bd774b805..dd2422446 100644 --- a/internal/web/job/clear_logs_job_test.go +++ b/internal/web/job/clear_logs_job_test.go @@ -19,14 +19,14 @@ func writeAccessLogConfig(t *testing.T, accessPath string) { if err != nil { 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) } } func TestWipeAccessLog_TruncatesEnabledLog(t *testing.T) { 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) } writeAccessLogConfig(t, accessLog) diff --git a/internal/web/job/xray_traffic_job.go b/internal/web/job/xray_traffic_job.go index dc3f6219d..9a89bd86d 100644 --- a/internal/web/job/xray_traffic_job.go +++ b/internal/web/job/xray_traffic_job.go @@ -178,7 +178,7 @@ func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traf defer fasthttp.ReleaseRequest(request) request.Header.SetMethod("POST") request.Header.SetContentType("application/json; charset=UTF-8") - request.SetBody([]byte(requestBody)) + request.SetBody(requestBody) request.SetRequestURI(informURL) response := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(response) diff --git a/internal/web/locale/locale.go b/internal/web/locale/locale.go index 32c35e67c..95e854b2f 100644 --- a/internal/web/locale/locale.go +++ b/internal/web/locale/locale.go @@ -56,7 +56,7 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { // createTemplateData creates a template data map from parameters with optional separator. func createTemplateData(params []string, separator ...string) map[string]any { - var sep string = "==" + sep := "==" if len(separator) > 0 { sep = separator[0] } diff --git a/internal/web/network/auto_https_conn.go b/internal/web/network/auto_https_conn.go index aa0e9deac..3dee0954c 100644 --- a/internal/web/network/auto_https_conn.go +++ b/internal/web/network/auto_https_conn.go @@ -50,7 +50,7 @@ func (c *AutoHttpsConn) readRequest() bool { resp.StatusCode = http.StatusTemporaryRedirect location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI) resp.Header.Set("Location", location) - resp.Write(c.Conn) + _ = resp.Write(c.Conn) c.Close() c.firstBuf = nil return true diff --git a/internal/web/service/client_apply_field_test.go b/internal/web/service/client_apply_field_test.go index b8368c075..56db2df2e 100644 --- a/internal/web/service/client_apply_field_test.go +++ b/internal/web/service/client_apply_field_test.go @@ -34,10 +34,14 @@ func TestResetClientExpiryTimeByEmail_MultiInbound(t *testing.T) { return string(b) } - first := &model.Inbound{Tag: "vless-a", Enable: true, Port: 50001, Protocol: model.VLESS, - StreamSettings: `{"network":"tcp","security":"reality"}`, Settings: clientJSON(oldExpiry)} - second := &model.Inbound{Tag: "vless-b", Enable: true, Port: 50002, Protocol: model.VLESS, - StreamSettings: `{"network":"ws","security":"tls"}`, Settings: clientJSON(oldExpiry)} + first := &model.Inbound{ + Tag: "vless-a", Enable: true, Port: 50001, Protocol: model.VLESS, + StreamSettings: `{"network":"tcp","security":"reality"}`, 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} { if err := db.Create(ib).Error; err != nil { t.Fatalf("create inbound %s: %v", ib.Tag, err) diff --git a/internal/web/service/client_bulk.go b/internal/web/service/client_bulk.go index f212fe798..3617390f0 100644 --- a/internal/web/service/client_bulk.go +++ b/internal/web/service/client_bulk.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/logger" diff --git a/internal/web/service/client_bulk_flow_test.go b/internal/web/service/client_bulk_flow_test.go index 06c302914..135c3ca07 100644 --- a/internal/web/service/client_bulk_flow_test.go +++ b/internal/web/service/client_bulk_flow_test.go @@ -40,8 +40,10 @@ func flowOf(t *testing.T, svc *ClientService, email string) string { return rec.Flow } -const realityStream = `{"network":"tcp","security":"reality"}` -const wsStream = `{"network":"ws","security":"none"}` +const ( + realityStream = `{"network":"tcp","security":"reality"}` + wsStream = `{"network":"ws","security":"none"}` +) // TestBulkAdjust_FlowSetAndClear covers the happy path: a vision flow is applied // on an eligible VLESS inbound and later cleared with the "none" directive. Both diff --git a/internal/web/service/client_crud.go b/internal/web/service/client_crud.go index 87b724c52..e322276d4 100644 --- a/internal/web/service/client_crud.go +++ b/internal/web/service/client_crud.go @@ -9,6 +9,7 @@ import ( "time" "github.com/google/uuid" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/util/common" diff --git a/internal/web/service/client_portable.go b/internal/web/service/client_portable.go index 706b9814b..192c204fe 100644 --- a/internal/web/service/client_portable.go +++ b/internal/web/service/client_portable.go @@ -5,6 +5,7 @@ import ( "time" "github.com/google/uuid" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" diff --git a/internal/web/service/client_traffic.go b/internal/web/service/client_traffic.go index 43ff1304a..486a275fc 100644 --- a/internal/web/service/client_traffic.go +++ b/internal/web/service/client_traffic.go @@ -84,7 +84,7 @@ func (s *ClientService) BulkResetTraffic(inboundSvc *InboundService, emails []st if err == nil && !rec.Enable { updated := rec.ToClient() updated.Enable = true - s.Update(inboundSvc, rec.Id, *updated) + _, _ = s.Update(inboundSvc, rec.Id, *updated) } } diff --git a/internal/web/service/del_shared_email_runtime_test.go b/internal/web/service/del_shared_email_runtime_test.go index b3092221a..a83a22e1a 100644 --- a/internal/web/service/del_shared_email_runtime_test.go +++ b/internal/web/service/del_shared_email_runtime_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/google/uuid" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) diff --git a/internal/web/service/email/email.go b/internal/web/service/email/email.go index db28cea89..2dfc78d5c 100644 --- a/internal/web/service/email/email.go +++ b/internal/web/service/email/email.go @@ -1,6 +1,7 @@ package email import ( + "context" "crypto/tls" "fmt" "net" @@ -115,10 +116,10 @@ func (s *EmailService) TestConnection() SMTPTestResult { switch encryptionType { case "tls": - conn, err = tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{ + conn, err = (&tls.Dialer{NetDialer: dialer, Config: &tls.Config{ ServerName: host, InsecureSkipVerify: false, - }) + }}).DialContext(context.Background(), "tcp", addr) default: 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 { // Dial with explicit timeout 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, InsecureSkipVerify: false, - }) + }}).DialContext(context.Background(), "tcp", addr) if err != nil { return err } @@ -289,7 +290,7 @@ func buildMessage(from string, to []string, subject, body string) []byte { } var msg strings.Builder 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(body) diff --git a/internal/web/service/fallback.go b/internal/web/service/fallback.go index acbab8f61..56812a580 100644 --- a/internal/web/service/fallback.go +++ b/internal/web/service/fallback.go @@ -1,6 +1,7 @@ package service import ( + "errors" "fmt" "strings" @@ -46,7 +47,7 @@ func (s *FallbackService) GetParentForChild(childId int) (*model.InboundFallback Where("child_id = ?", childId). Order("sort_order ASC, id ASC"). First(&row).Error - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { diff --git a/internal/web/service/inbound.go b/internal/web/service/inbound.go index 44ba5d2c8..96e92198e 100644 --- a/internal/web/service/inbound.go +++ b/internal/web/service/inbound.go @@ -5,6 +5,7 @@ package service import ( "context" "encoding/json" + "errors" "fmt" "net" "sort" @@ -153,7 +154,7 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound 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 } s.enrichClientStats(db, inbounds) @@ -196,7 +197,7 @@ func (s *InboundService) GetInboundsSlim(userId int) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound 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 } s.annotateFallbackParents(db, inbounds) @@ -319,7 +320,7 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) Where("user_id = ?", userId). Order("id ASC"). Scan(&rows).Error - if err != nil && err != gorm.ErrRecordNotFound { + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } out := make([]InboundOption, 0, len(rows)) @@ -343,7 +344,7 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound 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 } s.enrichClientStats(db, inbounds) @@ -354,7 +355,7 @@ func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbo db := database.GetDB() var inbounds []*model.Inbound 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 inbounds, nil @@ -362,7 +363,7 @@ func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbo func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) { settings := map[string][]model.Client{} - json.Unmarshal([]byte(inbound.Settings), &settings) + _ = json.Unmarshal([]byte(inbound.Settings), &settings) if settings == nil { return nil, fmt.Errorf("setting is null") } @@ -1348,7 +1349,7 @@ func (s *InboundService) GetInboundTags() (string, error) { db := database.GetDB() var inboundTags []string 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 } tags, _ := json.Marshal(inboundTags) @@ -1359,7 +1360,7 @@ func (s *InboundService) GetClientReverseTags() (string, error) { db := database.GetDB() var inbounds []model.Inbound 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 } @@ -1404,7 +1405,7 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) db := database.GetDB() var inbounds []*model.Inbound 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 inbounds, nil diff --git a/internal/web/service/inbound_clients.go b/internal/web/service/inbound_clients.go index eaf9950b9..133ec8133 100644 --- a/internal/web/service/inbound_clients.go +++ b/internal/web/service/inbound_clients.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/logger" diff --git a/internal/web/service/inbound_disable.go b/internal/web/service/inbound_disable.go index 721defba8..85d6f831f 100644 --- a/internal/web/service/inbound_disable.go +++ b/internal/web/service/inbound_disable.go @@ -27,7 +27,7 @@ func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error if err != nil { return false, 0, err } - s.xrayApi.Init(p.GetAPIPort()) + _ = s.xrayApi.Init(p.GetAPIPort()) for _, tag := range tags { err1 := s.xrayApi.DelInbound(tag) if err1 == nil { @@ -141,7 +141,7 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, []int, } if p != nil && len(localTargets) > 0 { - s.xrayApi.Init(p.GetAPIPort()) + _ = s.xrayApi.Init(p.GetAPIPort()) for _, t := range localTargets { err1 := s.xrayApi.RemoveUser(t.Tag, t.Email) if err1 == nil { @@ -231,7 +231,7 @@ func (s *InboundService) markClientsDisabledInSettings(tx *gorm.DB, inboundID in if _, hit := emails[email]; !hit { continue } - if cur, _ := entry["enable"].(bool); cur == false { + if cur, _ := entry["enable"].(bool); !cur { continue } entry["enable"] = false diff --git a/internal/web/service/inbound_migration.go b/internal/web/service/inbound_migration.go index fdc1f659f..70294cd08 100644 --- a/internal/web/service/inbound_migration.go +++ b/internal/web/service/inbound_migration.go @@ -2,6 +2,7 @@ package service import ( "encoding/json" + "errors" "fmt" "strconv" "strings" @@ -93,12 +94,12 @@ func (s *InboundService) MigrationRequirements() { // Fix inbounds based problems var inbounds []*model.Inbound 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 } for inbound_index := range inbounds { 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 { settings["clients"] = []any{} } @@ -117,7 +118,7 @@ func (s *InboundService) MigrationRequirements() { // Convert string tgId to int64 if _, ok := c["tgId"]; ok { - var tgId any = c["tgId"] + tgId := c["tgId"] if tgIdStr, ok2 := tgId.(string); ok2 { tgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, " ", ""), 10, 64) if err == nil { @@ -170,7 +171,7 @@ func (s *InboundService) MigrationRequirements() { var count int64 tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count) 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 { var reverses 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 settings, ok := tlsSettings["settings"].(map[string]any); ok { if domains, ok := settings["domains"].([]any); ok { diff --git a/internal/web/service/inbound_node.go b/internal/web/service/inbound_node.go index e83ba9376..cb23d70a0 100644 --- a/internal/web/service/inbound_node.go +++ b/internal/web/service/inbound_node.go @@ -999,7 +999,7 @@ func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) { db := database.GetDB() var rows []xray.ClientTraffic 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 } 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)) for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) { var page []xray.ClientTraffic - if err := db.Where("email IN ?", batch).Find(&page).Error; err != nil && err != gorm.ErrRecordNotFound { + if err := db.Where("email IN ?", batch).Find(&page).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, err } clients = append(clients, page...) diff --git a/internal/web/service/inbound_traffic.go b/internal/web/service/inbound_traffic.go index d99eefaf1..d77a11e5b 100644 --- a/internal/web/service/inbound_traffic.go +++ b/internal/web/service/inbound_traffic.go @@ -3,6 +3,7 @@ package service import ( "context" "encoding/json" + "errors" "fmt" "strings" "time" @@ -224,7 +225,7 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl } for inbound_index := range inbounds { 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) if ok { var newClients []any @@ -357,7 +358,7 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) { } for inbound_index := range inbounds { settings := map[string]any{} - json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) + _ = json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) clients, _ := settings["clients"].([]any) if len(clients) == 0 { continue @@ -760,7 +761,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) { continue } if len(newClients) == 0 { - s.DelInbound(inbound.Id) + _, _ = s.DelInbound(inbound.Id) continue } settings["clients"] = newClients @@ -827,7 +828,7 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi // Retrieve inbounds where settings contain the given tgId 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) return nil, err } @@ -853,7 +854,7 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) { var page []*xray.ClientTraffic 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 } 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 err = db.Model(model.Inbound{}).Where("settings LIKE ?", "%\""+query+"\"%").First(inbound).Error 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) return nil, err } @@ -1041,7 +1042,7 @@ func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.Client // Retrieve ClientTraffic based on the found email err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error 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) return nil, err } diff --git a/internal/web/service/inbound_update_tag_test.go b/internal/web/service/inbound_update_tag_test.go index 28f8c787b..38d61b230 100644 --- a/internal/web/service/inbound_update_tag_test.go +++ b/internal/web/service/inbound_update_tag_test.go @@ -49,7 +49,7 @@ func TestUpdateInbound_RegeneratesAutoTagOnPortChange(t *testing.T) { // the returned object) which is what the save would use. func TestUpdateInbound_NodeTagKeepsPrefixWhenNodeIdOmitted(t *testing.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 if err := database.GetDB().Where("tag = ?", "n1-in-443-tcp").First(&existing).Error; err != nil { diff --git a/internal/web/service/integration/nord.go b/internal/web/service/integration/nord.go index 3540f3388..a12138ea9 100644 --- a/internal/web/service/integration/nord.go +++ b/internal/web/service/integration/nord.go @@ -1,6 +1,7 @@ package integration import ( + "context" "encoding/json" "fmt" "io" @@ -21,7 +22,11 @@ var nordHTTPClient = &http.Client{Timeout: 15 * time.Second} const maxResponseSize = 10 << 20 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 { 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) - 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 { return "", err } @@ -89,7 +98,7 @@ func (s *NordService) SetKey(privateKey string) (string, error) { "token": "", } data, _ := json.Marshal(nordData) - err := s.SettingService.SetNord(string(data)) + err := s.SetNord(string(data)) if err != nil { return "", err } @@ -98,7 +107,7 @@ func (s *NordService) SetKey(privateKey string) (string, error) { func (s *NordService) GetCredentials(token string) (string, error) { 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 { return "", err } @@ -134,7 +143,7 @@ func (s *NordService) GetCredentials(token string) (string, error) { "token": token, } data, _ := json.Marshal(nordData) - err = s.SettingService.SetNord(string(data)) + err = s.SetNord(string(data)) if err != nil { return "", err } @@ -143,9 +152,9 @@ func (s *NordService) GetCredentials(token string) (string, error) { } func (s *NordService) GetNordData() (string, error) { - return s.SettingService.GetNord() + return s.GetNord() } func (s *NordService) DelNordData() error { - return s.SettingService.SetNord("") + return s.SetNord("") } diff --git a/internal/web/service/integration/warp.go b/internal/web/service/integration/warp.go index 5e22a4986..3f6c98997 100644 --- a/internal/web/service/integration/warp.go +++ b/internal/web/service/integration/warp.go @@ -2,6 +2,7 @@ package integration import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -27,11 +28,11 @@ const ( ) func (s *WarpService) GetWarpData() (string, error) { - return s.SettingService.GetWarp() + return s.GetWarp() } func (s *WarpService) DelWarpData() error { - return s.SettingService.SetWarp("") + return s.SetWarp("") } 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"]) - req, err := http.NewRequest(http.MethodGet, url, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) if err != nil { return "", err } @@ -67,7 +68,7 @@ func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error 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 { return "", err } @@ -116,7 +117,7 @@ func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error if err != nil { return "", err } - if err := s.SettingService.SetWarp(string(warpJSON)); err != nil { + if err := s.SetWarp(string(warpJSON)); err != nil { return "", err } @@ -142,7 +143,7 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) { 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 { return "", err } @@ -167,7 +168,7 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) { if err != nil { return "", err } - if err := s.SettingService.SetWarp(string(newWarpData)); err != nil { + if err := s.SetWarp(string(newWarpData)); err != nil { return "", err } 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. func (s *WarpService) loadWarpCreds() (map[string]string, error) { - warp, err := s.SettingService.GetWarp() + warp, err := s.GetWarp() if err != nil { return nil, err } diff --git a/internal/web/service/node.go b/internal/web/service/node.go index b3cbfef90..630eb5993 100644 --- a/internal/web/service/node.go +++ b/internal/web/service/node.go @@ -832,7 +832,7 @@ func (s *NodeService) withOutboundBridge(nodeID int, outboundTag string, fn func 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 { fn("") return diff --git a/internal/web/service/node_bulk_dispatch_test.go b/internal/web/service/node_bulk_dispatch_test.go index ec9ec2bd9..c67f8c612 100644 --- a/internal/web/service/node_bulk_dispatch_test.go +++ b/internal/web/service/node_bulk_dispatch_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/google/uuid" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "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) return nil } + func (f *fakeNodeRuntime) DeleteUser(context.Context, *model.Inbound, string) error { f.deleteUser.Add(1) return nil } + func (f *fakeNodeRuntime) AddClient(context.Context, *model.Inbound, model.Client) error { f.addClient.Add(1) return nil diff --git a/internal/web/service/node_client_traffic_sum_test.go b/internal/web/service/node_client_traffic_sum_test.go index c8b60f6a8..e16e6eb36 100644 --- a/internal/web/service/node_client_traffic_sum_test.go +++ b/internal/web/service/node_client_traffic_sum_test.go @@ -5,11 +5,12 @@ import ( "path/filepath" "testing" + "gorm.io/gorm" + "github.com/mhsanaei/3x-ui/v3/internal/database" "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/xray" - "gorm.io/gorm" ) func initTrafficTestDB(t *testing.T) *gorm.DB { diff --git a/internal/web/service/node_mtls_test.go b/internal/web/service/node_mtls_test.go index d7c438c91..704db997b 100644 --- a/internal/web/service/node_mtls_test.go +++ b/internal/web/service/node_mtls_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/go-playground/validator/v10" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) diff --git a/internal/web/service/outbound/outbound.go b/internal/web/service/outbound/outbound.go index 8e259225a..cffc27ccf 100644 --- a/internal/web/service/outbound/outbound.go +++ b/internal/web/service/outbound/outbound.go @@ -1,6 +1,7 @@ package outbound import ( + "context" "encoding/json" "fmt" "net" @@ -196,7 +197,7 @@ func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundRes func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult { r := TestEndpointResult{Address: endpoint} 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() if err != nil { r.Error = err.Error() diff --git a/internal/web/service/outbound/probe_http.go b/internal/web/service/outbound/probe_http.go index 072528ae5..a32c34e94 100644 --- a/internal/web/service/outbound/probe_http.go +++ b/internal/web/service/outbound/probe_http.go @@ -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) { var raw []json.RawMessage 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 { 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) { ports, release, err := reserveLoopbackPorts(len(items)) 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() @@ -261,14 +261,14 @@ func runHTTPProbeBatch(items []*httpBatchItem, allOutbounds []any, testURL strin configPath, err := createTestConfigPath() 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) proc := newBatchProcess(cfg, configPath) defer func() { 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 errors.Is(err, fs.ErrNotExist) { // 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 { @@ -330,7 +330,7 @@ func waitForPortsReady(proc batchProcess, ports []int, timeout time.Duration) *p if !proc.IsRunning() { 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 { conn.Close() break @@ -529,7 +529,7 @@ func reserveLoopbackPorts(n int) ([]int, func(), error) { } ports := make([]int, 0, 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 { release() return nil, nil, err diff --git a/internal/web/service/outbound_subscription.go b/internal/web/service/outbound_subscription.go index fa9e6f607..f5b08968c 100644 --- a/internal/web/service/outbound_subscription.go +++ b/internal/web/service/outbound_subscription.go @@ -281,7 +281,7 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript 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 { s.recordError(sub, err) return nil, err @@ -295,7 +295,7 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { err := fmt.Errorf("http %d", resp.StatusCode) s.recordError(sub, err) return nil, err diff --git a/internal/web/service/panel/panel.go b/internal/web/service/panel/panel.go index e4ed877cc..e53d5adc1 100644 --- a/internal/web/service/panel/panel.go +++ b/internal/web/service/panel/panel.go @@ -1,6 +1,7 @@ package panel import ( + "context" "encoding/json" "fmt" "io" @@ -157,7 +158,7 @@ func (s *PanelService) startUpdate(useDev bool) error { if systemdRun, err := exec.LookPath("systemd-run"); err == nil { unitName := fmt.Sprintf("x-ui-web-update-%d", time.Now().Unix()) - cmd := exec.Command(systemdRun, + cmd := exec.CommandContext(context.Background(), systemdRun, "--unit", unitName, "--setenv", "XUI_MAIN_FOLDER="+mainFolder, "--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(), "XUI_MAIN_FOLDER="+mainFolder, "XUI_SERVICE="+serviceFolder, @@ -199,7 +200,11 @@ func (s *PanelService) startUpdate(useDev bool) error { func downloadPanelUpdater() (string, error) { 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 { return "", fmt.Errorf("download panel updater: %w", err) } @@ -228,7 +233,7 @@ func downloadPanelUpdater() (string, error) { if n > 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 } 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 } 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 { return nil, err } diff --git a/internal/web/service/panel/user.go b/internal/web/service/panel/user.go index b66dcac89..fa058a2ee 100644 --- a/internal/web/service/panel/user.go +++ b/internal/web/service/panel/user.go @@ -3,14 +3,15 @@ package panel import ( "errors" + "github.com/xlzd/gotp" + "gorm.io/gorm" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap" "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. @@ -43,7 +44,7 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode Where("username = ?", username). First(user). Error - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("invalid credentials") } else if err != nil { logger.Warning("check user err:", err) @@ -89,7 +90,6 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode if twoFactorEnable { twoFactorToken, err := s.settingService.GetTwoFactorToken() - if err != nil { logger.Warning("check two factor token err:", err) return nil, err @@ -114,7 +114,6 @@ func (s *UserService) BumpLoginEpoch() error { func (s *UserService) UpdateUser(id int, username string, password string) error { db := database.GetDB() hashedPassword, err := crypto.HashPasswordAsBcrypt(password) - if err != nil { return err } @@ -125,8 +124,8 @@ func (s *UserService) UpdateUser(id int, username string, password string) error } if twoFactorEnable { - s.settingService.SetTwoFactorEnable(false) - s.settingService.SetTwoFactorToken("") + _ = s.settingService.SetTwoFactorEnable(false) + _ = s.settingService.SetTwoFactorToken("") } return db.Model(model.User{}). diff --git a/internal/web/service/panel/websocket.go b/internal/web/service/panel/websocket.go index e401a7199..1d00123eb 100644 --- a/internal/web/service/panel/websocket.go +++ b/internal/web/service/panel/websocket.go @@ -65,7 +65,7 @@ func (s *WebSocketService) readPump(client *websocket.Client, conn *ws.Conn) { }() conn.SetReadLimit(wsClientReadLimit) - conn.SetReadDeadline(time.Now().Add(wsPongWait)) + _ = conn.SetReadDeadline(time.Now().Add(wsPongWait)) conn.SetPongHandler(func(string) error { return conn.SetReadDeadline(time.Now().Add(wsPongWait)) }) @@ -94,9 +94,9 @@ func (s *WebSocketService) writePump(client *websocket.Client, conn *ws.Conn) { for { select { case msg, ok := <-client.Send: - conn.SetWriteDeadline(time.Now().Add(wsWriteWait)) + _ = conn.SetWriteDeadline(time.Now().Add(wsWriteWait)) if !ok { - conn.WriteMessage(ws.CloseMessage, []byte{}) + _ = conn.WriteMessage(ws.CloseMessage, []byte{}) return } 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: - conn.SetWriteDeadline(time.Now().Add(wsWriteWait)) + _ = conn.SetWriteDeadline(time.Now().Add(wsWriteWait)) if err := conn.WriteMessage(ws.PingMessage, nil); err != nil { logger.Debugf("WebSocket ping error for client %s: %v", client.ID, err) return diff --git a/internal/web/service/port_conflict_test.go b/internal/web/service/port_conflict_test.go index b8d91080a..329caa0f6 100644 --- a/internal/web/service/port_conflict_test.go +++ b/internal/web/service/port_conflict_test.go @@ -6,10 +6,11 @@ import ( "sync" "testing" + "github.com/op/go-logging" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" 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 @@ -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) { cases := []struct { name string @@ -410,7 +408,7 @@ func TestResolveInboundTag_RespectsCallerTagWhenFree(t *testing.T) { Port: 5000, Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, - NodeID: intPtr(1), + NodeID: new(1), } got, err := svc.resolveInboundTag(pushed, 0) if err != nil { @@ -481,7 +479,7 @@ func TestGenerateInboundTag_NodePrefix(t *testing.T) { Listen: "0.0.0.0", Port: 443, Protocol: model.VLESS, - NodeID: intPtr(1), + NodeID: new(1), } got, err := svc.generateInboundTag(in, 0) if err != nil { @@ -503,7 +501,7 @@ func TestGenerateInboundTag_NodePrefixedDoesNotCollideWithLocal(t *testing.T) { Listen: "0.0.0.0", Port: 443, Protocol: model.VLESS, - NodeID: intPtr(1), + NodeID: new(1), } got, err := svc.generateInboundTag(in, 0) if err != nil { @@ -653,7 +651,7 @@ func TestIsAutoGeneratedTag(t *testing.T) { {"canonical", "in-443-tcp", 443, nil, tcp, true}, {"canonical udp", "in-443-udp", 443, nil, transportUDP, 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}, {"custom tag", "my-cool-tag", 443, nil, tcp, false}, {"stale port", "in-443-tcp", 8443, nil, tcp, false}, @@ -708,7 +706,7 @@ func TestCheckPortConflict_ReservedAPIPortAllowedOnNode(t *testing.T) { Port: defaultXrayAPIPort, Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, - NodeID: intPtr(1), + NodeID: new(1), } 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) diff --git a/internal/web/service/server.go b/internal/web/service/server.go index 2d98a2880..2f9ba8684 100644 --- a/internal/web/service/server.go +++ b/internal/web/service/server.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bufio" "bytes" + "context" "crypto/sha256" "crypto/x509" "encoding/hex" @@ -233,7 +234,7 @@ func (s *ServerService) isFail2banInstalled() bool { return s.fail2banInstalled } - err := exec.Command("fail2ban-client", "-h").Run() + err := exec.CommandContext(context.Background(), "fail2ban-client", "-h").Run() s.fail2banInstalled = err == nil s.fail2banCheckedAt = time.Now() return s.fail2banInstalled @@ -351,7 +352,11 @@ func getPublicIP(url string) string { 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 { return "N/A" } @@ -772,7 +777,11 @@ func (s *ServerService) GetXrayVersions() ([]string, error) { 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 { return nil, err } @@ -872,7 +881,11 @@ func (s *ServerService) downloadXRay(version string) (string, error) { fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName) 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 { return "", err } @@ -934,7 +947,11 @@ func (s *ServerService) downloadXRay(version string) (string, error) { // fetchXrayDigestSHA256 downloads the .dgst sidecar XTLS publishes next to each // release asset and returns the SHA2-256 hex digest it lists. 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 { return "", fmt.Errorf("download xray checksum: %w", err) } @@ -1009,7 +1026,7 @@ func (s *ServerService) UpdateXray(version string) error { return err } 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 } tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*") @@ -1031,7 +1048,7 @@ func (s *ServerService) UpdateXray(version string) error { if n > 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 } 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 - 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 cmd.Stdout = &out err = cmd.Run() @@ -1121,8 +1138,8 @@ func (s *ServerService) GetXrayLogs( showBlocked string, showProxy string, freedoms []string, - blackholes []string) []LogEntry { - + blackholes []string, +) []LogEntry { const ( Direct = iota Blocked @@ -1149,12 +1166,12 @@ func (s *ServerService) GetXrayLogs( line := strings.TrimSpace(scanner.Text()) if line == "" || strings.Contains(line, "api -> api") { - //skipping empty lines and api calls + // skipping empty lines and api calls continue } if filter != "" && !strings.Contains(line, filter) { - //applying filter if it's not empty + // applying filter if it's not empty continue } @@ -1580,7 +1597,7 @@ func (s *ServerService) exportPostgresDB() ([]byte, error) { if err != nil { 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 var out, stderr bytes.Buffer 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) } - cmd := exec.Command(bin, + cmd := exec.CommandContext(context.Background(), bin, "--clean", "--if-exists", "--no-owner", "--no-privileges", "--single-transaction", "--dbname", dbname, tempPath, ) @@ -1721,7 +1738,7 @@ func (s *ServerService) UpdateGeofile(fileName string) error { downloadFile := func(url, destPath string) error { var req *http.Request - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) if err != nil { 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) { // Run the command - cmd := exec.Command(xray.GetBinaryPath(), "x25519") + cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "x25519") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() @@ -1844,7 +1861,7 @@ func (s *ServerService) GetNewX25519Cert() (any, error) { func (s *ServerService) GetNewmldsa65() (any, error) { // Run the command - cmd := exec.Command(xray.GetBinaryPath(), "mldsa65") + cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "mldsa65") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() @@ -2048,7 +2065,7 @@ func (s *ServerService) GetRemoteCertHash(server string) ([]string, error) { func (s *ServerService) GetNewEchCert(sni string) (any, error) { // 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 cmd.Stdout = &out err := cmd.Run() @@ -2071,7 +2088,7 @@ func (s *ServerService) GetNewEchCert(sni string) (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 cmd.Stdout = &out if err := cmd.Run(); err != nil { @@ -2166,7 +2183,7 @@ func (s *ServerService) GetNewUUID() (map[string]string, error) { func (s *ServerService) GetNewmlkem768() (any, error) { // Run the command - cmd := exec.Command(xray.GetBinaryPath(), "mlkem768") + cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "mlkem768") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() diff --git a/internal/web/service/setting.go b/internal/web/service/setting.go index 13d7682cd..276eb5cf8 100644 --- a/internal/web/service/setting.go +++ b/internal/web/service/setting.go @@ -14,6 +14,7 @@ import ( "time" "github.com/google/uuid" + "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" diff --git a/internal/web/service/sync_scale_postgres_test.go b/internal/web/service/sync_scale_postgres_test.go index 4bfed7686..9b111c32f 100644 --- a/internal/web/service/sync_scale_postgres_test.go +++ b/internal/web/service/sync_scale_postgres_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" diff --git a/internal/web/service/tgbot/tgbot.go b/internal/web/service/tgbot/tgbot.go index d76f78c03..a2ea32a99 100644 --- a/internal/web/service/tgbot/tgbot.go +++ b/internal/web/service/tgbot/tgbot.go @@ -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) return err } - parsedAdminIds = append(parsedAdminIds, int64(id)) + parsedAdminIds = append(parsedAdminIds, id) } } tgBotMutex.Lock() @@ -472,7 +472,7 @@ func StopBot() { userStateMgr.reset() if handler != nil { - handler.Stop() + _ = handler.Stop() } if cancel != nil { diff --git a/internal/web/service/tgbot/tgbot_client.go b/internal/web/service/tgbot/tgbot_client.go index d050aad93..50c008025 100644 --- a/internal/web/service/tgbot/tgbot_client.go +++ b/internal/web/service/tgbot/tgbot_client.go @@ -74,13 +74,13 @@ func (t *Tgbot) BuildClientDraftMessage() string { var b strings.Builder b.WriteString("πŸ“ *New client draft*\r\n") - b.WriteString(fmt.Sprintf("πŸ“§ Email: `%s`\r\n", client_Email)) - b.WriteString(fmt.Sprintf("πŸ”— Attached: %s\r\n", attached)) - b.WriteString(fmt.Sprintf("πŸ“Š Traffic: %s\r\n", traffic)) - b.WriteString(fmt.Sprintf("πŸ“… Expire: %s\r\n", expiry)) - b.WriteString(fmt.Sprintf("πŸ”’ IP limit: %s\r\n", ipLimit)) - b.WriteString(fmt.Sprintf("πŸ‘€ TG user: %s\r\n", tgID)) - b.WriteString(fmt.Sprintf("πŸ’¬ Comment: %s\r\n", comment)) + fmt.Fprintf(&b, "πŸ“§ Email: `%s`\r\n", client_Email) + fmt.Fprintf(&b, "πŸ”— Attached: %s\r\n", attached) + fmt.Fprintf(&b, "πŸ“Š Traffic: %s\r\n", traffic) + fmt.Fprintf(&b, "πŸ“… Expire: %s\r\n", expiry) + fmt.Fprintf(&b, "πŸ”’ IP limit: %s\r\n", ipLimit) + fmt.Fprintf(&b, "πŸ‘€ TG user: %s\r\n", tgID) + fmt.Fprintf(&b, "πŸ’¬ Comment: %s\r\n", comment) return b.String() } @@ -216,7 +216,6 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) { } subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID) } else { - 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. - req, err := http.NewRequest("GET", subURL, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, subURL, nil) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) return @@ -376,7 +375,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) { // Also generate a few individual links' QRs (first up to 5) subPageURL := subURL - req, err := http.NewRequest("GET", subPageURL, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, subPageURL, nil) if err == nil { req.Header.Set("Accept", "text/plain, */*;q=0.1") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) diff --git a/internal/web/service/tgbot/tgbot_inbound.go b/internal/web/service/tgbot/tgbot_inbound.go index 7b4cc4b26..c17272e8b 100644 --- a/internal/web/service/tgbot/tgbot_inbound.go +++ b/internal/web/service/tgbot/tgbot_inbound.go @@ -31,7 +31,7 @@ func (t *Tgbot) getInboundUsages() string { clients, listErr := t.clientService.ListForInbound(nil, inbound.Id) 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 { @@ -126,11 +126,9 @@ func (t *Tgbot) getInboundClientsFor(inboundID int, action string) (*telego.Inli for _, client := range clients { buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery(action+" "+client.Email))) } - } else { return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed")) } - } cols := 0 if len(buttons) < 6 { @@ -252,11 +250,9 @@ func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) for _, client := range clients { buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery("client_get_usage "+client.Email))) } - } else { return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed")) } - } cols := 0 if len(buttons) < 6 { diff --git a/internal/web/service/tgbot/tgbot_report.go b/internal/web/service/tgbot/tgbot_report.go index 39214e9ac..61dff6cba 100644 --- a/internal/web/service/tgbot/tgbot_report.go +++ b/internal/web/service/tgbot/tgbot_report.go @@ -49,7 +49,7 @@ func (t *Tgbot) SendBackupToAdmins() { return } for i, adminId := range adminIds { - t.sendBackup(int64(adminId)) + t.sendBackup(adminId) // Add delay between sends to avoid Telegram rate limits if i < len(adminIds)-1 { time.Sleep(1 * time.Second) @@ -63,7 +63,7 @@ func (t *Tgbot) sendExhaustedToAdmins() { return } for _, adminId := range adminIds { - t.getExhausted(int64(adminId)) + t.getExhausted(adminId) } } diff --git a/internal/web/service/tgbot/tgbot_router.go b/internal/web/service/tgbot/tgbot_router.go index 75f89be6f..08ef2847e 100644 --- a/internal/web/service/tgbot/tgbot_router.go +++ b/internal/web/service/tgbot/tgbot_router.go @@ -141,7 +141,6 @@ func (t *Tgbot) OnReceive() { userStateMgr.clear(message.Chat.ID) t.addClient(message.Chat.ID, t.BuildClientDraftMessage()) } - } else { if message.UsersShared != nil { if checkAdmin(message.From.ID) { @@ -167,7 +166,7 @@ func (t *Tgbot) OnReceive() { return nil }, th.AnyMessage()) - h.Start() + _ = h.Start() }() } @@ -205,7 +204,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo if isAdmin { t.searchClient(chatId, commandArgs[0]) } else { - t.getClientUsage(chatId, int64(message.From.ID), commandArgs[0]) + t.getClientUsage(chatId, message.From.ID, commandArgs[0]) } } else { 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-time.Now().Unix()*1000 < 0 { - date = -int64(days * 24 * 60 * 60000) + date = -(days * 24 * 60 * 60000) } else { - date = traffic.ExpiryTime + int64(days*24*60*60000) + date = traffic.ExpiryTime + days*24*60*60000 } } 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 if client_ExpiryTime > 0 { if client_ExpiryTime-time.Now().Unix()*1000 < 0 { - date = -int64(days * 24 * 60 * 60000) + date = -(days * 24 * 60 * 60000) } else { - date = client_ExpiryTime + int64(days*24*60*60000) + date = client_ExpiryTime + days*24*60*60000 } } else { - date = client_ExpiryTime - int64(days*24*60*60000) + date = client_ExpiryTime - days*24*60*60000 } 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) } - } } @@ -1458,7 +1456,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool case "get_sorted_traffic_usage_report": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) emails, err := t.inboundService.GetAllEmails() - if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) return diff --git a/internal/web/service/xray.go b/internal/web/service/xray.go index 3c3d710b1..77cb7b117 100644 --- a/internal/web/service/xray.go +++ b/internal/web/service/xray.go @@ -143,7 +143,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { continue } 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) if listErr != nil { @@ -240,7 +240,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { if len(inbound.StreamSettings) > 0 { // Unmarshal stream JSON 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" 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") return nil } - p.Stop() + _ = p.Stop() } p = xray.NewProcess(xrayConfig) diff --git a/internal/web/service/xray_setting.go b/internal/web/service/xray_setting.go index 1f2db1c07..0682b3959 100644 --- a/internal/web/service/xray_setting.go +++ b/internal/web/service/xray_setting.go @@ -29,7 +29,7 @@ func (s *XraySettingService) SaveXraySetting(newXraySettings string) error { if hoisted, err := EnsureStatsRouting(newXraySettings); err == nil { newXraySettings = hoisted } - return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings) + return s.saveSetting("xrayTemplateConfig", newXraySettings) } func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { diff --git a/internal/web/web.go b/internal/web/web.go index a61b45d3e..9868dfc38 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -309,10 +309,10 @@ func (s *Server) startTask(restartXray bool) { } } // 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 - s.cron.AddFunc(cadenceXrayRestart, func() { + _, _ = s.cron.AddFunc(cadenceXrayRestart, func() { if s.xrayService.IsNeedRestartAndSetFalse() { err := s.xrayService.RestartXray(false) if err != nil { @@ -323,37 +323,37 @@ func (s *Server) startTask(restartXray bool) { go func() { 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 mtJob := job.NewMtprotoJob() - s.cron.AddJob(cadenceMtproto, mtJob) + _, _ = s.cron.AddJob(cadenceMtproto, mtJob) go mtJob.Run() // 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) - s.cron.AddJob(cadenceOutboundSub, job.NewOutboundSubscriptionJob()) + _, _ = s.cron.AddJob(cadenceOutboundSub, job.NewOutboundSubscriptionJob()) // check client ips from log file every day - s.cron.AddJob("@daily", job.NewClearLogsJob()) - s.cron.AddJob("@hourly", job.NewWarpIpJob()) + _, _ = s.cron.AddJob("@daily", job.NewClearLogsJob()) + _, _ = s.cron.AddJob("@hourly", job.NewWarpIpJob()) // Inbound traffic reset jobs // Run every hour - s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly")) + _, _ = s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly")) // 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 - s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly")) + _, _ = s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly")) // 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 if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled { @@ -363,7 +363,7 @@ func (s *Server) startTask(restartXray bool) { } j := job.NewLdapSyncJob() // job has zero-value services with method receivers that read settings on demand - s.cron.AddJob(runtime, j) + _, _ = s.cron.AddJob(runtime, j) } // Telegram-bot–dependent 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 - s.cron.AddJob(cadenceCheckHash, job.NewCheckHashStorageJob()) + _, _ = s.cron.AddJob(cadenceCheckHash, job.NewCheckHashStorageJob()) } // CPU monitor publishes cpu.high events; register it whenever any notifier // (Telegram or Email) wants them, independent of the Telegram bot being on. 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. if s.memoryAlarmWanted() { - s.cron.AddJob(cadenceMemoryAlarm, job.NewCheckMemJob()) + _, _ = s.cron.AddJob(cadenceMemoryAlarm, job.NewCheckMemJob()) } 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() { time.Sleep(time.Minute) 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 defer func() { 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)) - listener, err := net.Listen("tcp", listenAddr) + listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", listenAddr) if err != nil { return err } @@ -593,7 +593,7 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) { } go func() { - s.httpServer.Serve(listener) + _ = s.httpServer.Serve(listener) }() // 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() if (err == nil) && (isTgbotenabled) { tgBot := s.tgbotService.NewTgbot() - tgBot.Start(i18nFS) + _ = tgBot.Start(i18nFS) // Subscribe Telegram notifications for event bus 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 { s.cancel() if stopXray { - s.xrayService.StopXray() + _ = s.xrayService.StopXray() mtproto.GetManager().StopAll() } if s.cron != nil { diff --git a/internal/web/websocket/hub_test.go b/internal/web/websocket/hub_test.go index dbb7336bf..231480f4f 100644 --- a/internal/web/websocket/hub_test.go +++ b/internal/web/websocket/hub_test.go @@ -7,8 +7,9 @@ import ( "testing" "time" - xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/op/go-logging" + + xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" ) func TestMain(m *testing.M) { diff --git a/internal/xray/process.go b/internal/xray/process.go index 4e6342709..4735a2e99 100644 --- a/internal/xray/process.go +++ b/internal/xray/process.go @@ -2,6 +2,7 @@ package xray import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -74,7 +75,7 @@ func GetAccessLogPath() (string, error) { } jsonConfig := map[string]any{} - err = json.Unmarshal([]byte(config), &jsonConfig) + err = json.Unmarshal(config, &jsonConfig) if err != nil { logger.Warningf("Failed to parse JSON configuration: %s", err) return "", err @@ -92,7 +93,7 @@ func GetAccessLogPath() (string, error) { // stopProcess calls Stop on the given Process instance. func stopProcess(p *Process) { - p.Stop() + _ = p.Stop() } // 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. func (p *process) refreshVersion() { - cmd := exec.Command(GetBinaryPath(), "-version") + cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "-version") data, err := cmd.Output() if err != nil { p.version = "Unknown" @@ -521,7 +522,7 @@ func (p *process) Start() (err error) { 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.Stderr = p.logWriter diff --git a/internal/xray/process_test.go b/internal/xray/process_test.go index 24314bf49..cf034a0cb 100644 --- a/internal/xray/process_test.go +++ b/internal/xray/process_test.go @@ -12,8 +12,9 @@ import ( "testing" "time" - xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/op/go-logging" + + xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" ) func TestWriteFileAtomicModeAndRenameFailure(t *testing.T) { @@ -203,7 +204,7 @@ func markProcessHelperReady(t *testing.T) { if readyPath == "" { 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) } } diff --git a/internal/xray/process_windows.go b/internal/xray/process_windows.go index 96c63ed52..8b9eb0dd0 100644 --- a/internal/xray/process_windows.go +++ b/internal/xray/process_windows.go @@ -7,8 +7,9 @@ import ( "sync" "unsafe" - "github.com/mhsanaei/3x-ui/v3/internal/logger" "golang.org/x/sys/windows" + + "github.com/mhsanaei/3x-ui/v3/internal/logger" ) var ( @@ -36,7 +37,7 @@ func ensureKillOnExitJob() (windows.Handle, error) { uint32(unsafe.Sizeof(info)), ) if err != nil { - windows.CloseHandle(h) + _ = windows.CloseHandle(h) killOnExitJobErr = err return } @@ -59,7 +60,7 @@ func attachChildLifetime(cmd *exec.Cmd) { logger.Warning("xray: OpenProcess for job attach failed:", err) return } - defer windows.CloseHandle(h) + defer func() { _ = windows.CloseHandle(h) }() if err := windows.AssignProcessToJobObject(job, h); err != nil { logger.Warning("xray: AssignProcessToJobObject failed:", err) } diff --git a/main.go b/main.go index b32ea43f3..ec2379ab8 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,7 @@ func runWebServer() { log.Fatalf("Unknown log level: %v", config.GetLogLevel()) } - godotenv.Load() + _ = godotenv.Load() for _, line := range sys.ApplyMemoryTuning() { logger.Info(line) @@ -171,8 +171,8 @@ func runWebServer() { tgbot.StopBot() // ------------------------------------------------------------ - server.Stop() - subServer.Stop() + _ = server.Stop() + _ = subServer.Stop() log.Println("Shutting down servers.") return } @@ -350,7 +350,7 @@ func updateSetting(port int, username string, password string, webBasePath strin if err != nil { fmt.Println("Failed to reset two-factor authentication:", err) } else { - settingService.SetTwoFactorToken("") + _ = settingService.SetTwoFactorToken("") fmt.Println("Two-factor authentication reset successfully") } } diff --git a/tools/openapigen/walker.go b/tools/openapigen/walker.go index caeb7d76b..36aeb0af3 100644 --- a/tools/openapigen/walker.go +++ b/tools/openapigen/walker.go @@ -57,9 +57,7 @@ func walkPackages(requests []packageRequest) ([]Schema, []Alias, error) { } overrides := req.Overrides[ts.Name.Name] for _, fld := range strct.Fields.List { - for _, f := range buildFields(fld, overrides) { - s.Fields = append(s.Fields, f) - } + s.Fields = append(s.Fields, buildFields(fld, overrides)...) } schemas = append(schemas, s) continue