mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-27 16:14:21 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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, '}')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user