mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-24 05:26:08 +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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user