style: adopt golangci-lint v2 and resolve all findings

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

Resolve all 217 findings behavior-preserving: gofumpt/goimports formatting, explicit blank assignment on intentionally ignored errors, errors.Is/errors.As and %w wrapping, context-aware stdlib calls (CommandContext/QueryContext/NewRequestWithContext/Dialer), staticcheck simplifications, removed redundant conversions, http.StatusOK and http.MethodGet, inlined the go:fix intPtr helper, and deferred sql rows Close. Add a golangci CI job mirroring the existing Go jobs.
This commit is contained in:
MHSanaei
2026-06-27 15:42:22 +02:00
parent 7efa0d9ddd
commit fa1a19c03c
81 changed files with 410 additions and 286 deletions
+15
View File
@@ -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:
+53
View File
@@ -0,0 +1,53 @@
version: "2"
run:
build-tags: []
timeout: 5m
linters:
default: standard
enable:
- bodyclose
- errorlint
- noctx
- misspell
- rowserrcheck
- sqlclosecheck
- unconvert
- usestdlibvars
exclusions:
generated: lax
presets:
- std-error-handling
paths:
- frontend
- internal/web/dist
rules:
- path: _test\.go
linters:
- errcheck
- bodyclose
- noctx
# tools/openapigen relies on go/parser.ParseDir; migrating it to
# golang.org/x/tools/go/packages is a generator change, out of scope here.
- linters:
- staticcheck
text: "SA1019: parser.ParseDir"
# ST1005 (capitalized error strings) conflicts with intentional
# user-facing error copy that tests assert verbatim.
- linters:
- staticcheck
text: "ST1005:"
formatters:
enable:
- gofumpt
- goimports
settings:
goimports:
local-prefixes:
- github.com/mhsanaei/3x-ui
exclusions:
paths:
- frontend
- internal/web/dist
@@ -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) {
+6 -6
View File
@@ -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
}
}
+8 -11
View File
@@ -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
}
+2 -2
View File
@@ -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.
+2 -1
View File
@@ -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) {
+2 -1
View File
@@ -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"
)
+11 -6
View File
@@ -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, '}')
+2 -1
View File
@@ -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
+4 -3
View File
@@ -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)
}
+3 -3
View File
@@ -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":
+3 -2
View File
@@ -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)
+2 -1
View File
@@ -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
}
+2 -1
View File
@@ -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 {
+7 -7
View File
@@ -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),
+9 -9
View File
@@ -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
}
}
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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) {
+2 -1
View File
@@ -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) {
+1 -1
View File
@@ -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)
}
}
+2 -1
View File
@@ -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")
-2
View File
@@ -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
-1
View File
@@ -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)
+6 -7
View File
@@ -42,7 +42,6 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
// initRouter sets up the routes for server status, Xray management, and utility endpoints.
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.
+8 -7
View File
@@ -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
}
}
@@ -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
+1 -4
View File
@@ -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 {
+1 -4
View File
@@ -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 {
+4 -4
View File
@@ -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()
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -56,7 +56,7 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
// createTemplateData creates a template data map from parameters with optional separator.
func createTemplateData(params []string, separator ...string) map[string]any {
var sep string = "=="
sep := "=="
if len(separator) > 0 {
sep = separator[0]
}
+1 -1
View File
@@ -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
@@ -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)
+1
View File
@@ -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"
@@ -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
+1
View File
@@ -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"
+1
View File
@@ -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"
+1 -1
View File
@@ -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)
}
}
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
+6 -5
View File
@@ -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)
+2 -1
View File
@@ -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 {
+10 -9
View File
@@ -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
+1
View File
@@ -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"
+3 -3
View File
@@ -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
+6 -5
View File
@@ -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 {
+2 -2
View File
@@ -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...)
+8 -7
View File
@@ -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
}
@@ -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 {
+16 -7
View File
@@ -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("")
}
+9 -8
View File
@@ -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
}
+1 -1
View File
@@ -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
@@ -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
@@ -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 {
+1
View File
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/go-playground/validator/v10"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
+2 -1
View File
@@ -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()
+8 -8
View File
@@ -101,7 +101,7 @@ func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allO
func (s *OutboundService) TestOutbounds(outboundsJSON string, testURL string, allOutboundsJSON string, mode string) ([]*TestOutboundResult, error) {
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
@@ -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
+14 -5
View File
@@ -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
}
+6 -7
View File
@@ -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{}).
+4 -4
View File
@@ -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
+7 -9
View File
@@ -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)
+37 -20
View File
@@ -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()
+1
View File
@@ -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"
@@ -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"
+2 -2
View File
@@ -283,7 +283,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
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 {
+9 -10
View File
@@ -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)
+1 -5
View File
@@ -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 {
+2 -2
View File
@@ -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)
}
}
+8 -11
View File
@@ -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
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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 {
+24 -24
View File
@@ -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-botdependent jobs: periodic stats report + callback-hash cleanup.
@@ -383,21 +383,21 @@ func (s *Server) startTask(restartXray bool) {
}
// check for Telegram bot callback query hash storage reset
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 {
+2 -1
View File
@@ -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) {
+5 -4
View File
@@ -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
+3 -2
View File
@@ -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)
}
}
+4 -3
View File
@@ -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)
}
+4 -4
View File
@@ -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")
}
}
+1 -3
View File
@@ -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