diff --git a/config/config.go b/config/config.go index d5fe65ff..c9a3e83c 100644 --- a/config/config.go +++ b/config/config.go @@ -1,3 +1,5 @@ +// Package config provides configuration management utilities for the 3x-ui panel, +// including version information, logging levels, database paths, and environment variable handling. package config import ( @@ -16,8 +18,10 @@ var version string //go:embed name var name string +// LogLevel represents the logging level for the application. type LogLevel string +// Logging level constants const ( Debug LogLevel = "debug" Info LogLevel = "info" @@ -26,14 +30,17 @@ const ( Error LogLevel = "error" ) +// GetVersion returns the version string of the 3x-ui application. func GetVersion() string { return strings.TrimSpace(version) } +// GetName returns the name of the 3x-ui application. func GetName() string { return strings.TrimSpace(name) } +// GetLogLevel returns the current logging level based on environment variables or defaults to Info. func GetLogLevel() LogLevel { if IsDebug() { return Debug @@ -45,10 +52,12 @@ func GetLogLevel() LogLevel { return LogLevel(logLevel) } +// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable. func IsDebug() bool { return os.Getenv("XUI_DEBUG") == "true" } +// GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER. func GetBinFolderPath() string { binFolderPath := os.Getenv("XUI_BIN_FOLDER") if binFolderPath == "" { @@ -74,6 +83,7 @@ func getBaseDir() string { return exeDir } +// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults. func GetDBFolderPath() string { dbFolderPath := os.Getenv("XUI_DB_FOLDER") if dbFolderPath != "" { @@ -85,10 +95,12 @@ func GetDBFolderPath() string { return "/etc/x-ui" } +// GetDBPath returns the full path to the database file. func GetDBPath() string { return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) } +// GetLogFolder returns the path to the log folder based on environment variables or platform defaults. func GetLogFolder() string { logFolderPath := os.Getenv("XUI_LOG_FOLDER") if logFolderPath != "" { diff --git a/database/db.go b/database/db.go index 8414b118..6de81d79 100644 --- a/database/db.go +++ b/database/db.go @@ -1,3 +1,5 @@ +// Package database provides database initialization, migration, and management utilities +// for the 3x-ui panel using GORM with SQLite. package database import ( @@ -45,6 +47,7 @@ func initModels() error { return nil } +// initUser creates a default admin user if the users table is empty. func initUser() error { empty, err := isTableEmpty("users") if err != nil { @@ -68,6 +71,7 @@ func initUser() error { return nil } +// runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running. func runSeeders(isUsersEmpty bool) error { empty, err := isTableEmpty("history_of_seeders") if err != nil { @@ -107,12 +111,14 @@ func runSeeders(isUsersEmpty bool) error { return nil } +// isTableEmpty returns true if the named table contains zero rows. func isTableEmpty(tableName string) (bool, error) { var count int64 err := db.Table(tableName).Count(&count).Error return count == 0, err } +// InitDB sets up the database connection, migrates models, and runs seeders. func InitDB(dbPath string) error { dir := path.Dir(dbPath) err := os.MkdirAll(dir, fs.ModePerm) @@ -151,6 +157,7 @@ func InitDB(dbPath string) error { return runSeeders(isUsersEmpty) } +// CloseDB closes the database connection if it exists. func CloseDB() error { if db != nil { sqlDB, err := db.DB() @@ -162,14 +169,17 @@ func CloseDB() error { return nil } +// GetDB returns the global GORM database instance. func GetDB() *gorm.DB { return db } +// IsNotFound checks if the given error is a GORM record not found error. func IsNotFound(err error) bool { return err == gorm.ErrRecordNotFound } +// IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature. func IsSQLiteDB(file io.ReaderAt) (bool, error) { signature := []byte("SQLite format 3\x00") buf := make([]byte, len(signature)) @@ -180,6 +190,7 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) { return bytes.Equal(buf, signature), nil } +// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency. func Checkpoint() error { // Update WAL err := db.Exec("PRAGMA wal_checkpoint;").Error diff --git a/database/model/model.go b/database/model/model.go index abf8075c..720f0223 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -1,3 +1,4 @@ +// Package model defines the database models and data structures used by the 3x-ui panel. package model import ( @@ -7,8 +8,10 @@ import ( "github.com/mhsanaei/3x-ui/v2/xray" ) +// Protocol represents the protocol type for Xray inbounds. type Protocol string +// Protocol constants for different Xray inbound protocols const ( VMESS Protocol = "vmess" VLESS Protocol = "vless" @@ -20,27 +23,29 @@ const ( WireGuard Protocol = "wireguard" ) +// User represents a user account in the 3x-ui panel. type User struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` Username string `json:"username"` Password string `json:"password"` } +// Inbound represents an Xray inbound configuration with traffic statistics and settings. type Inbound struct { - Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` - UserId int `json:"-"` - Up int64 `json:"up" form:"up"` - Down int64 `json:"down" form:"down"` - Total int64 `json:"total" form:"total"` - AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` - Remark string `json:"remark" form:"remark"` - Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` - ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` - TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` - LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` - ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` + Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier + UserId int `json:"-"` // Associated user ID + Up int64 `json:"up" form:"up"` // Upload traffic in bytes + Down int64 `json:"down" form:"down"` // Download traffic in bytes + Total int64 `json:"total" form:"total"` // Total traffic limit in bytes + AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage + Remark string `json:"remark" form:"remark"` // Human-readable remark + Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp + TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule + LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp + ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics - // config part + // Xray configuration fields Listen string `json:"listen" form:"listen"` Port int `json:"port" form:"port"` Protocol Protocol `json:"protocol" form:"protocol"` @@ -50,6 +55,7 @@ type Inbound struct { Sniffing string `json:"sniffing" form:"sniffing"` } +// OutboundTraffics tracks traffic statistics for Xray outbound connections. type OutboundTraffics struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Tag string `json:"tag" form:"tag" gorm:"unique"` @@ -58,17 +64,20 @@ type OutboundTraffics struct { Total int64 `json:"total" form:"total" gorm:"default:0"` } +// InboundClientIps stores IP addresses associated with inbound clients for access control. type InboundClientIps struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` Ips string `json:"ips" form:"ips"` } +// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running. type HistoryOfSeeders struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` SeederName string `json:"seederName"` } +// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { listen := i.Listen if listen != "" { @@ -85,33 +94,36 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { } } +// Setting stores key-value configuration settings for the 3x-ui panel. type Setting struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Key string `json:"key" form:"key"` Value string `json:"value" form:"value"` } +// Client represents a client configuration for Xray inbounds with traffic limits and settings. type Client struct { - ID string `json:"id"` - Security string `json:"security"` - Password string `json:"password"` - Flow string `json:"flow"` - Email string `json:"email"` - LimitIP int `json:"limitIp"` - TotalGB int64 `json:"totalGB" form:"totalGB"` - ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` - Enable bool `json:"enable" form:"enable"` - TgID int64 `json:"tgId" form:"tgId"` - SubID string `json:"subId" form:"subId"` - Comment string `json:"comment" form:"comment"` - Reset int `json:"reset" form:"reset"` - CreatedAt int64 `json:"created_at,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` + ID string `json:"id"` // Unique client identifier + Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm") + Password string `json:"password"` // Client password + Flow string `json:"flow"` // Flow control (XTLS) + Email string `json:"email"` // Client email identifier + LimitIP int `json:"limitIp"` // IP limit for this client + TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp + Enable bool `json:"enable" form:"enable"` // Whether the client is enabled + TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications + SubID string `json:"subId" form:"subId"` // Subscription identifier + Comment string `json:"comment" form:"comment"` // Client comment + Reset int `json:"reset" form:"reset"` // Reset period in days + CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp + UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp } +// VLESSSettings contains VLESS protocol-specific configuration settings. type VLESSSettings struct { - Clients []Client `json:"clients"` - Decryption string `json:"decryption"` - Encryption string `json:"encryption"` - Fallbacks []any `json:"fallbacks"` + Clients []Client `json:"clients"` // List of VLESS clients + Decryption string `json:"decryption"` // Decryption method + Encryption string `json:"encryption"` // Encryption method (usually "none" for VLESS) + Fallbacks []any `json:"fallbacks"` // Fallback configurations } diff --git a/logger/logger.go b/logger/logger.go index 3705c3df..ccacf697 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -1,3 +1,5 @@ +// Package logger provides logging functionality for the 3x-ui panel with +// buffered log storage and multiple log levels. package logger import ( @@ -9,7 +11,11 @@ import ( ) var ( - logger *logging.Logger + logger *logging.Logger + + // addToBuffer appends a log entry into the in-memory ring buffer used for + // retrieving recent logs via the web UI. It keeps the buffer bounded to avoid + // uncontrolled growth. logBuffer []struct { time string level logging.Level @@ -21,6 +27,7 @@ func init() { InitLogger(logging.INFO) } +// InitLogger initializes the logger with the specified logging level. func InitLogger(level logging.Level) { newLogger := logging.MustGetLogger("x-ui") var err error @@ -47,51 +54,61 @@ func InitLogger(level logging.Level) { logger = newLogger } +// Debug logs a debug message and adds it to the log buffer. func Debug(args ...any) { logger.Debug(args...) addToBuffer("DEBUG", fmt.Sprint(args...)) } +// Debugf logs a formatted debug message and adds it to the log buffer. func Debugf(format string, args ...any) { logger.Debugf(format, args...) addToBuffer("DEBUG", fmt.Sprintf(format, args...)) } +// Info logs an info message and adds it to the log buffer. func Info(args ...any) { logger.Info(args...) addToBuffer("INFO", fmt.Sprint(args...)) } +// Infof logs a formatted info message and adds it to the log buffer. func Infof(format string, args ...any) { logger.Infof(format, args...) addToBuffer("INFO", fmt.Sprintf(format, args...)) } +// Notice logs a notice message and adds it to the log buffer. func Notice(args ...any) { logger.Notice(args...) addToBuffer("NOTICE", fmt.Sprint(args...)) } +// Noticef logs a formatted notice message and adds it to the log buffer. func Noticef(format string, args ...any) { logger.Noticef(format, args...) addToBuffer("NOTICE", fmt.Sprintf(format, args...)) } +// Warning logs a warning message and adds it to the log buffer. func Warning(args ...any) { logger.Warning(args...) addToBuffer("WARNING", fmt.Sprint(args...)) } +// Warningf logs a formatted warning message and adds it to the log buffer. func Warningf(format string, args ...any) { logger.Warningf(format, args...) addToBuffer("WARNING", fmt.Sprintf(format, args...)) } +// Error logs an error message and adds it to the log buffer. func Error(args ...any) { logger.Error(args...) addToBuffer("ERROR", fmt.Sprint(args...)) } +// Errorf logs a formatted error message and adds it to the log buffer. func Errorf(format string, args ...any) { logger.Errorf(format, args...) addToBuffer("ERROR", fmt.Sprintf(format, args...)) @@ -115,6 +132,7 @@ func addToBuffer(level string, newLog string) { }) } +// GetLogs retrieves up to c log entries from the buffer that are at or below the specified level. func GetLogs(c int, level string) []string { var output []string logLevel, _ := logging.LogLevel(level) diff --git a/main.go b/main.go index d02a37eb..119dc4d9 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,5 @@ +// Package main is the entry point for the 3x-ui web panel application. +// It initializes the database, web server, and handles command-line operations for managing the panel. package main import ( @@ -22,6 +24,7 @@ import ( "github.com/op/go-logging" ) +// runWebServer initializes and starts the web server for the 3x-ui panel. func runWebServer() { log.Printf("Starting %v %v", config.GetName(), config.GetVersion()) @@ -111,6 +114,7 @@ func runWebServer() { } } +// resetSetting resets all panel settings to their default values. func resetSetting() { err := database.InitDB(config.GetDBPath()) if err != nil { @@ -127,6 +131,7 @@ func resetSetting() { } } +// showSetting displays the current panel settings if show is true. func showSetting(show bool) { if show { settingService := service.SettingService{} @@ -176,6 +181,7 @@ func showSetting(show bool) { } } +// updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter. func updateTgbotEnableSts(status bool) { settingService := service.SettingService{} currentTgSts, err := settingService.GetTgbotEnabled() @@ -195,6 +201,7 @@ func updateTgbotEnableSts(status bool) { } } +// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule. func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) { err := database.InitDB(config.GetDBPath()) if err != nil { @@ -232,6 +239,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri } } +// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication. func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) { err := database.InitDB(config.GetDBPath()) if err != nil { @@ -290,6 +298,7 @@ func updateSetting(port int, username string, password string, webBasePath strin } } +// updateCert updates the SSL certificate files for the panel. func updateCert(publicKey string, privateKey string) { err := database.InitDB(config.GetDBPath()) if err != nil { @@ -317,6 +326,7 @@ func updateCert(publicKey string, privateKey string) { } } +// GetCertificate displays the current SSL certificate settings if getCert is true. func GetCertificate(getCert bool) { if getCert { settingService := service.SettingService{} @@ -334,6 +344,7 @@ func GetCertificate(getCert bool) { } } +// GetListenIP displays the current panel listen IP address if getListen is true. func GetListenIP(getListen bool) { if getListen { @@ -348,6 +359,7 @@ func GetListenIP(getListen bool) { } } +// migrateDb performs database migration operations for the 3x-ui panel. func migrateDb() { inboundService := service.InboundService{} @@ -360,6 +372,8 @@ func migrateDb() { fmt.Println("Migration done!") } +// main is the entry point of the 3x-ui application. +// It parses command-line arguments to run the web server, migrate database, or update settings. func main() { if len(os.Args) < 2 { runWebServer() diff --git a/sub/sub.go b/sub/sub.go index 448842ae..c5445339 100644 --- a/sub/sub.go +++ b/sub/sub.go @@ -1,3 +1,5 @@ +// Package sub provides subscription server functionality for the 3x-ui panel, +// including HTTP/HTTPS servers for serving subscription links and JSON configurations. package sub import ( @@ -39,6 +41,7 @@ func setEmbeddedTemplates(engine *gin.Engine) error { return nil } +// Server represents the subscription server that serves subscription links and JSON configurations. type Server struct { httpServer *http.Server listener net.Listener @@ -50,6 +53,7 @@ type Server struct { cancel context.CancelFunc } +// NewServer creates a new subscription server instance with a cancellable context. func NewServer() *Server { ctx, cancel := context.WithCancel(context.Background()) return &Server{ @@ -58,6 +62,8 @@ func NewServer() *Server { } } +// initRouter configures the subscription server's Gin engine, middleware, +// templates and static assets and returns the ready-to-use engine. func (s *Server) initRouter() (*gin.Engine, error) { // Always run in release mode for the subscription server gin.DefaultWriter = io.Discard @@ -222,6 +228,7 @@ func (s *Server) getHtmlFiles() ([]string, error) { return files, nil } +// Start initializes and starts the subscription server with configured settings. func (s *Server) Start() (err error) { // This is an anonymous function, no function name defer func() { @@ -295,6 +302,7 @@ func (s *Server) Start() (err error) { return nil } +// Stop gracefully shuts down the subscription server and closes the listener. func (s *Server) Stop() error { s.cancel() @@ -309,6 +317,7 @@ func (s *Server) Stop() error { return common.Combine(err1, err2) } +// GetCtx returns the server's context for cancellation and deadline management. func (s *Server) GetCtx() context.Context { return s.ctx } diff --git a/sub/subController.go b/sub/subController.go index d6bc0923..42a33ee6 100644 --- a/sub/subController.go +++ b/sub/subController.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" ) +// SUBController handles HTTP requests for subscription links and JSON configurations. type SUBController struct { subTitle string subPath string @@ -22,6 +23,7 @@ type SUBController struct { subJsonService *SubJsonService } +// NewSUBController creates a new subscription controller with the given configuration. func NewSUBController( g *gin.RouterGroup, subPath string, @@ -53,6 +55,8 @@ func NewSUBController( return a } +// initRouter registers HTTP routes for subscription links and JSON endpoints +// on the provided router group. func (a *SUBController) initRouter(g *gin.RouterGroup) { gLink := g.Group(a.subPath) gLink.GET(":subid", a.subs) @@ -62,6 +66,7 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) { } } +// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data. func (a *SUBController) subs(c *gin.Context) { subId := c.Param("subid") scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c) @@ -119,6 +124,7 @@ func (a *SUBController) subs(c *gin.Context) { } } +// subJsons handles HTTP requests for JSON subscription configurations. func (a *SUBController) subJsons(c *gin.Context) { subId := c.Param("subid") _, host, _, _ := a.subService.ResolveRequest(c) @@ -134,6 +140,7 @@ func (a *SUBController) subJsons(c *gin.Context) { } } +// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title. func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) { c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Profile-Update-Interval", updateInterval) diff --git a/sub/subJsonService.go b/sub/subJsonService.go index d55c7f81..f440ab65 100644 --- a/sub/subJsonService.go +++ b/sub/subJsonService.go @@ -17,6 +17,7 @@ import ( //go:embed default.json var defaultJson string +// SubJsonService handles JSON subscription configuration generation and management. type SubJsonService struct { configJson map[string]any defaultOutbounds []json_util.RawMessage @@ -28,6 +29,7 @@ type SubJsonService struct { SubService *SubService } +// NewSubJsonService creates a new JSON subscription service with the given configuration. func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService { var configJson map[string]any var defaultOutbounds []json_util.RawMessage @@ -67,6 +69,7 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string, } } +// GetJson generates a JSON subscription configuration for the given subscription ID and host. func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) { inbounds, err := s.SubService.getInboundsBySubId(subId) if err != nil || len(inbounds) == 0 { diff --git a/sub/subService.go b/sub/subService.go index 206be24e..9f28b35b 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -20,6 +20,7 @@ import ( "github.com/mhsanaei/3x-ui/v2/xray" ) +// SubService provides business logic for generating subscription links and managing subscription data. type SubService struct { address string showInfo bool @@ -29,6 +30,7 @@ type SubService struct { settingService service.SettingService } +// NewSubService creates a new subscription service with the given configuration. func NewSubService(showInfo bool, remarkModel string) *SubService { return &SubService{ showInfo: showInfo, @@ -36,6 +38,7 @@ func NewSubService(showInfo bool, remarkModel string) *SubService { } } +// GetSubs retrieves subscription links for a given subscription ID and host. func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) { s.address = host var result []string @@ -1008,6 +1011,7 @@ func searchHost(headers any) string { } // PageData is a view model for subpage.html +// PageData contains data for rendering the subscription information page. type PageData struct { Host string BasePath string @@ -1029,6 +1033,7 @@ type PageData struct { } // ResolveRequest extracts scheme and host info from request/headers consistently. +// ResolveRequest extracts scheme, host, and header information from an HTTP request. func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) { // scheme scheme = "http" @@ -1072,6 +1077,7 @@ func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, } // BuildURLs constructs absolute subscription and json URLs. +// BuildURLs constructs subscription and JSON subscription URLs for a given subscription ID. func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) { if strings.HasSuffix(subPath, "/") { subURL = scheme + "://" + hostWithPort + subPath + subId @@ -1087,6 +1093,7 @@ func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId } // BuildPageData parses header and prepares the template view model. +// BuildPageData constructs page data for rendering the subscription information page. func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string) PageData { download := common.FormatTraffic(traffic.Down) upload := common.FormatTraffic(traffic.Up) diff --git a/util/common/err.go b/util/common/err.go index 85a743ad..e12bd13f 100644 --- a/util/common/err.go +++ b/util/common/err.go @@ -1,3 +1,4 @@ +// Package common provides common utility functions for error handling, formatting, and multi-error management. package common import ( @@ -7,16 +8,19 @@ import ( "github.com/mhsanaei/3x-ui/v2/logger" ) +// NewErrorf creates a new error with formatted message. func NewErrorf(format string, a ...any) error { msg := fmt.Sprintf(format, a...) return errors.New(msg) } +// NewError creates a new error from the given arguments. func NewError(a ...any) error { msg := fmt.Sprintln(a...) return errors.New(msg) } +// Recover handles panic recovery and logs the panic error if a message is provided. func Recover(msg string) any { panicErr := recover() if panicErr != nil { diff --git a/util/common/format.go b/util/common/format.go index c73e3a01..c40bd3dc 100644 --- a/util/common/format.go +++ b/util/common/format.go @@ -4,6 +4,7 @@ import ( "fmt" ) +// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB). func FormatTraffic(trafficBytes int64) string { units := []string{"B", "KB", "MB", "GB", "TB", "PB"} unitIndex := 0 diff --git a/util/common/multi_error.go b/util/common/multi_error.go index ff9ff628..c695e3c0 100644 --- a/util/common/multi_error.go +++ b/util/common/multi_error.go @@ -4,8 +4,10 @@ import ( "strings" ) +// multiError represents a collection of errors. type multiError []error +// Error returns a string representation of all errors joined with " | ". func (e multiError) Error() string { var r strings.Builder r.WriteString("multierr: ") @@ -16,6 +18,7 @@ func (e multiError) Error() string { return r.String() } +// Combine combines multiple errors into a single error, filtering out nil errors. func Combine(maybeError ...error) error { var errs multiError for _, err := range maybeError { diff --git a/util/crypto/crypto.go b/util/crypto/crypto.go index f600e7a6..05d088a8 100644 --- a/util/crypto/crypto.go +++ b/util/crypto/crypto.go @@ -1,14 +1,17 @@ +// Package crypto provides cryptographic utilities for password hashing and verification. package crypto import ( "golang.org/x/crypto/bcrypt" ) +// HashPasswordAsBcrypt generates a bcrypt hash of the given password. func HashPasswordAsBcrypt(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(hash), err } +// CheckPasswordHash verifies if the given password matches the bcrypt hash. func CheckPasswordHash(hash, password string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil diff --git a/util/json_util/json.go b/util/json_util/json.go index 54e3728a..d2d391bf 100644 --- a/util/json_util/json.go +++ b/util/json_util/json.go @@ -1,12 +1,15 @@ +// Package json_util provides JSON utilities including a custom RawMessage type. package json_util import ( "errors" ) +// RawMessage is a custom JSON raw message type that marshals empty slices as "null". type RawMessage []byte -// MarshalJSON: Customize json.RawMessage default behavior +// MarshalJSON customizes the JSON marshaling behavior for RawMessage. +// Empty RawMessage values are marshaled as "null" instead of "[]". func (m RawMessage) MarshalJSON() ([]byte, error) { if len(m) == 0 { return []byte("null"), nil @@ -14,7 +17,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) { return m, nil } -// UnmarshalJSON: sets *m to a copy of data. +// UnmarshalJSON sets *m to a copy of the JSON data. func (m *RawMessage) UnmarshalJSON(data []byte) error { if m == nil { return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") diff --git a/util/random/random.go b/util/random/random.go index 67ee0691..9610e26c 100644 --- a/util/random/random.go +++ b/util/random/random.go @@ -1,3 +1,4 @@ +// Package random provides utilities for generating random strings and numbers. package random import ( @@ -13,6 +14,8 @@ var ( allSeq [62]rune ) +// init initializes the character sequences used for random string generation. +// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations. func init() { for i := 0; i < 10; i++ { numSeq[i] = rune('0' + i) @@ -33,6 +36,7 @@ func init() { copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:]) } +// Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters). func Seq(n int) string { runes := make([]rune, n) for i := 0; i < n; i++ { @@ -41,6 +45,7 @@ func Seq(n int) string { return string(runes) } +// Num generates a random integer between 0 and n-1. func Num(n int) int { return rand.Intn(n) } diff --git a/util/reflect_util/reflect.go b/util/reflect_util/reflect.go index 1fdaec50..1f557e0d 100644 --- a/util/reflect_util/reflect.go +++ b/util/reflect_util/reflect.go @@ -1,7 +1,9 @@ +// Package reflect_util provides reflection utilities for working with struct fields and values. package reflect_util import "reflect" +// GetFields returns all struct fields of the given reflect.Type. func GetFields(t reflect.Type) []reflect.StructField { num := t.NumField() fields := make([]reflect.StructField, 0, num) @@ -11,6 +13,7 @@ func GetFields(t reflect.Type) []reflect.StructField { return fields } +// GetFieldValues returns all field values of the given reflect.Value. func GetFieldValues(v reflect.Value) []reflect.Value { num := v.NumField() fields := make([]reflect.Value, 0, num) diff --git a/util/sys/psutil.go b/util/sys/psutil.go index 3d7cac80..98adf775 100644 --- a/util/sys/psutil.go +++ b/util/sys/psutil.go @@ -1,3 +1,5 @@ +// Package sys provides system utilities for monitoring network connections and CPU usage. +// Platform-specific implementations are provided for Windows, Linux, and macOS. package sys import ( diff --git a/util/sys/sys_linux.go b/util/sys/sys_linux.go index 8a494d62..23483b57 100644 --- a/util/sys/sys_linux.go +++ b/util/sys/sys_linux.go @@ -45,6 +45,8 @@ func getLinesNum(filename string) (int, error) { return sum, nil } +// GetTCPCount returns the number of active TCP connections by reading +// /proc/net/tcp and /proc/net/tcp6 when available. func GetTCPCount() (int, error) { root := HostProc() @@ -75,6 +77,8 @@ func GetUDPCount() (int, error) { return udp4 + udp6, nil } +// safeGetLinesNum returns 0 if the file does not exist, otherwise forwards +// to getLinesNum to count the number of lines. func safeGetLinesNum(path string) (int, error) { if _, err := os.Stat(path); os.IsNotExist(err) { return 0, nil diff --git a/util/sys/sys_windows.go b/util/sys/sys_windows.go index f3eae076..186fa4bb 100644 --- a/util/sys/sys_windows.go +++ b/util/sys/sys_windows.go @@ -12,6 +12,7 @@ import ( "github.com/shirou/gopsutil/v4/net" ) +// GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp"). func GetConnectionCount(proto string) (int, error) { if proto != "tcp" && proto != "udp" { return 0, errors.New("invalid protocol") @@ -24,10 +25,12 @@ func GetConnectionCount(proto string) (int, error) { return len(stats), nil } +// GetTCPCount returns the number of active TCP connections. func GetTCPCount() (int, error) { return GetConnectionCount("tcp") } +// GetUDPCount returns the number of active UDP connections. func GetUDPCount() (int, error) { return GetConnectionCount("udp") } @@ -50,6 +53,8 @@ type filetime struct { HighDateTime uint32 } +// ftToUint64 converts a Windows FILETIME-like struct to a uint64 for +// arithmetic and delta calculations used by CPUPercentRaw. func ftToUint64(ft filetime) uint64 { return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime) } diff --git a/web/controller/api.go b/web/controller/api.go index 506f8cc3..dbd3f28d 100644 --- a/web/controller/api.go +++ b/web/controller/api.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" ) +// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management. type APIController struct { BaseController inboundController *InboundController @@ -13,12 +14,14 @@ type APIController struct { Tgbot service.Tgbot } +// NewAPIController creates a new APIController instance and initializes its routes. func NewAPIController(g *gin.RouterGroup) *APIController { a := &APIController{} a.initRouter(g) return a } +// initRouter sets up the API routes for inbounds, server, and other endpoints. func (a *APIController) initRouter(g *gin.RouterGroup) { // Main API group api := g.Group("/panel/api") @@ -36,6 +39,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) { api.GET("/backuptotgbot", a.BackuptoTgbot) } +// BackuptoTgbot sends a backup of the panel data to Telegram bot admins. func (a *APIController) BackuptoTgbot(c *gin.Context) { a.Tgbot.SendBackupToAdmins() } diff --git a/web/controller/base.go b/web/controller/base.go index 15e8cb57..7bc61b64 100644 --- a/web/controller/base.go +++ b/web/controller/base.go @@ -1,3 +1,5 @@ +// Package controller provides HTTP request handlers and controllers for the 3x-ui web management panel. +// It handles routing, authentication, and API endpoints for managing Xray inbounds, settings, and more. package controller import ( @@ -10,8 +12,10 @@ import ( "github.com/gin-gonic/gin" ) +// BaseController provides common functionality for all controllers, including authentication checks. type BaseController struct{} +// checkLogin is a middleware that verifies user authentication and handles unauthorized access. func (a *BaseController) checkLogin(c *gin.Context) { if !session.IsLogin(c) { if isAjax(c) { @@ -25,6 +29,7 @@ func (a *BaseController) checkLogin(c *gin.Context) { } } +// I18nWeb retrieves an internationalized message for the web interface based on the current locale. func I18nWeb(c *gin.Context, name string, params ...string) string { anyfunc, funcExists := c.Get("I18n") if !funcExists { diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 0a988506..eeb160d6 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -12,17 +12,20 @@ import ( "github.com/gin-gonic/gin" ) +// InboundController handles HTTP requests related to Xray inbounds management. type InboundController struct { inboundService service.InboundService xrayService service.XrayService } +// NewInboundController creates a new InboundController and sets up its routes. func NewInboundController(g *gin.RouterGroup) *InboundController { a := &InboundController{} a.initRouter(g) return a } +// initRouter initializes the routes for inbound-related operations. func (a *InboundController) initRouter(g *gin.RouterGroup) { g.GET("/list", a.getInbounds) @@ -49,6 +52,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) { g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail) } +// getInbounds retrieves the list of inbounds for the logged-in user. func (a *InboundController) getInbounds(c *gin.Context) { user := session.GetLoginUser(c) inbounds, err := a.inboundService.GetInbounds(user.Id) @@ -59,6 +63,7 @@ func (a *InboundController) getInbounds(c *gin.Context) { jsonObj(c, inbounds, nil) } +// getInbound retrieves a specific inbound by its ID. func (a *InboundController) getInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -73,6 +78,7 @@ func (a *InboundController) getInbound(c *gin.Context) { jsonObj(c, inbound, nil) } +// getClientTraffics retrieves client traffic information by email. func (a *InboundController) getClientTraffics(c *gin.Context) { email := c.Param("email") clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email) @@ -83,6 +89,7 @@ func (a *InboundController) getClientTraffics(c *gin.Context) { jsonObj(c, clientTraffics, nil) } +// getClientTrafficsById retrieves client traffic information by inbound ID. func (a *InboundController) getClientTrafficsById(c *gin.Context) { id := c.Param("id") clientTraffics, err := a.inboundService.GetClientTrafficByID(id) @@ -93,6 +100,7 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) { jsonObj(c, clientTraffics, nil) } +// addInbound creates a new inbound configuration. func (a *InboundController) addInbound(c *gin.Context) { inbound := &model.Inbound{} err := c.ShouldBind(inbound) @@ -119,6 +127,7 @@ func (a *InboundController) addInbound(c *gin.Context) { } } +// delInbound deletes an inbound configuration by its ID. func (a *InboundController) delInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -136,6 +145,7 @@ func (a *InboundController) delInbound(c *gin.Context) { } } +// updateInbound updates an existing inbound configuration. func (a *InboundController) updateInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -161,6 +171,7 @@ func (a *InboundController) updateInbound(c *gin.Context) { } } +// getClientIps retrieves the IP addresses associated with a client by email. func (a *InboundController) getClientIps(c *gin.Context) { email := c.Param("email") @@ -173,6 +184,7 @@ func (a *InboundController) getClientIps(c *gin.Context) { jsonObj(c, ips, nil) } +// clearClientIps clears the IP addresses for a client by email. func (a *InboundController) clearClientIps(c *gin.Context) { email := c.Param("email") @@ -184,6 +196,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil) } +// addInboundClient adds a new client to an existing inbound. func (a *InboundController) addInboundClient(c *gin.Context) { data := &model.Inbound{} err := c.ShouldBind(data) @@ -203,6 +216,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) { } } +// delInboundClient deletes a client from an inbound by inbound ID and client ID. func (a *InboundController) delInboundClient(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -222,6 +236,7 @@ func (a *InboundController) delInboundClient(c *gin.Context) { } } +// updateInboundClient updates a client's configuration in an inbound. func (a *InboundController) updateInboundClient(c *gin.Context) { clientId := c.Param("clientId") @@ -243,6 +258,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) { } } +// resetClientTraffic resets the traffic counter for a specific client in an inbound. func (a *InboundController) resetClientTraffic(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -262,6 +278,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) { } } +// resetAllTraffics resets all traffic counters across all inbounds. func (a *InboundController) resetAllTraffics(c *gin.Context) { err := a.inboundService.ResetAllTraffics() if err != nil { @@ -273,6 +290,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil) } +// resetAllClientTraffics resets traffic counters for all clients in a specific inbound. func (a *InboundController) resetAllClientTraffics(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -290,6 +308,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil) } +// importInbound imports an inbound configuration from provided data. func (a *InboundController) importInbound(c *gin.Context) { inbound := &model.Inbound{} err := json.Unmarshal([]byte(c.PostForm("data")), inbound) @@ -319,6 +338,7 @@ func (a *InboundController) importInbound(c *gin.Context) { } } +// delDepletedClients deletes clients in an inbound who have exhausted their traffic limits. func (a *InboundController) delDepletedClients(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -333,15 +353,18 @@ func (a *InboundController) delDepletedClients(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil) } +// onlines retrieves the list of currently online clients. func (a *InboundController) onlines(c *gin.Context) { jsonObj(c, a.inboundService.GetOnlineClients(), nil) } +// lastOnline retrieves the last online timestamps for clients. func (a *InboundController) lastOnline(c *gin.Context) { data, err := a.inboundService.GetClientsLastOnline() jsonObj(c, data, err) } +// updateClientTraffic updates the traffic statistics for a client by email. func (a *InboundController) updateClientTraffic(c *gin.Context) { email := c.Param("email") @@ -367,6 +390,7 @@ func (a *InboundController) updateClientTraffic(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) } +// delInboundClientByEmail deletes a client from an inbound by email address. func (a *InboundController) delInboundClientByEmail(c *gin.Context) { inboundId, err := strconv.Atoi(c.Param("id")) if err != nil { diff --git a/web/controller/index.go b/web/controller/index.go index f21e3128..89de710b 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -13,12 +13,14 @@ import ( "github.com/gin-gonic/gin" ) +// LoginForm represents the login request structure. type LoginForm struct { Username string `json:"username" form:"username"` Password string `json:"password" form:"password"` TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"` } +// IndexController handles the main index and login-related routes. type IndexController struct { BaseController @@ -27,12 +29,14 @@ type IndexController struct { tgbot service.Tgbot } +// NewIndexController creates a new IndexController and initializes its routes. func NewIndexController(g *gin.RouterGroup) *IndexController { a := &IndexController{} a.initRouter(g) return a } +// initRouter sets up the routes for index, login, logout, and two-factor authentication. func (a *IndexController) initRouter(g *gin.RouterGroup) { g.GET("/", a.index) g.POST("/login", a.login) @@ -40,6 +44,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) { g.POST("/getTwoFactorEnable", a.getTwoFactorEnable) } +// index handles the root route, redirecting logged-in users to the panel or showing the login page. func (a *IndexController) index(c *gin.Context) { if session.IsLogin(c) { c.Redirect(http.StatusTemporaryRedirect, "panel/") @@ -48,6 +53,7 @@ func (a *IndexController) index(c *gin.Context) { html(c, "login.html", "pages.login.title", nil) } +// login handles user authentication and session creation. func (a *IndexController) login(c *gin.Context) { var form LoginForm @@ -95,6 +101,7 @@ func (a *IndexController) login(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil) } +// logout handles user logout by clearing the session and redirecting to the login page. func (a *IndexController) logout(c *gin.Context) { user := session.GetLoginUser(c) if user != nil { @@ -107,6 +114,7 @@ func (a *IndexController) logout(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) } +// getTwoFactorEnable retrieves the current status of two-factor authentication. func (a *IndexController) getTwoFactorEnable(c *gin.Context) { status, err := a.settingService.GetTwoFactorEnable() if err == nil { diff --git a/web/controller/server.go b/web/controller/server.go index 768adb52..60d165c5 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -15,6 +15,7 @@ import ( var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`) +// ServerController handles server management and status-related operations. type ServerController struct { BaseController @@ -27,6 +28,7 @@ type ServerController struct { lastGetVersionsTime int64 // unix seconds } +// NewServerController creates a new ServerController, initializes routes, and starts background tasks. func NewServerController(g *gin.RouterGroup) *ServerController { a := &ServerController{} a.initRouter(g) @@ -34,6 +36,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController { return a } +// 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) @@ -58,6 +61,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.POST("/getNewEchCert", a.getNewEchCert) } +// refreshStatus updates the cached server status and collects CPU history. func (a *ServerController) refreshStatus() { a.lastStatus = a.serverService.GetStatus(a.lastStatus) // collect cpu history when status is fresh @@ -66,6 +70,7 @@ func (a *ServerController) refreshStatus() { } } +// startTask initiates background tasks for continuous status monitoring. func (a *ServerController) startTask() { webServer := global.GetWebServer() c := webServer.GetCron() @@ -76,8 +81,10 @@ func (a *ServerController) startTask() { }) } +// status returns the current server status information. func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) } +// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket. func (a *ServerController) getCpuHistoryBucket(c *gin.Context) { bucketStr := c.Param("bucket") bucket, err := strconv.Atoi(bucketStr) @@ -101,6 +108,7 @@ func (a *ServerController) getCpuHistoryBucket(c *gin.Context) { jsonObj(c, points, nil) } +// getXrayVersion retrieves available Xray versions, with caching for 1 minute. func (a *ServerController) getXrayVersion(c *gin.Context) { now := time.Now().Unix() if now-a.lastGetVersionsTime <= 60 { // 1 minute cache @@ -120,18 +128,21 @@ func (a *ServerController) getXrayVersion(c *gin.Context) { jsonObj(c, versions, nil) } +// installXray installs or updates Xray to the specified version. func (a *ServerController) installXray(c *gin.Context) { version := c.Param("version") err := a.serverService.UpdateXray(version) jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err) } +// updateGeofile updates the specified geo file for Xray. func (a *ServerController) updateGeofile(c *gin.Context) { fileName := c.Param("fileName") err := a.serverService.UpdateGeofile(fileName) jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err) } +// stopXrayService stops the Xray service. func (a *ServerController) stopXrayService(c *gin.Context) { err := a.serverService.StopXrayService() if err != nil { @@ -141,6 +152,7 @@ func (a *ServerController) stopXrayService(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err) } +// restartXrayService restarts the Xray service. func (a *ServerController) restartXrayService(c *gin.Context) { err := a.serverService.RestartXrayService() if err != nil { @@ -150,6 +162,7 @@ func (a *ServerController) restartXrayService(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err) } +// getLogs retrieves the application logs based on count, level, and syslog filters. func (a *ServerController) getLogs(c *gin.Context) { count := c.Param("count") level := c.PostForm("level") @@ -158,6 +171,7 @@ func (a *ServerController) getLogs(c *gin.Context) { jsonObj(c, logs, nil) } +// getXrayLogs retrieves Xray logs with filtering options for direct, blocked, and proxy traffic. func (a *ServerController) getXrayLogs(c *gin.Context) { count := c.Param("count") filter := c.PostForm("filter") @@ -202,6 +216,7 @@ func (a *ServerController) getXrayLogs(c *gin.Context) { jsonObj(c, logs, nil) } +// getConfigJson retrieves the Xray configuration as JSON. func (a *ServerController) getConfigJson(c *gin.Context) { configJson, err := a.serverService.GetConfigJson() if err != nil { @@ -211,6 +226,7 @@ func (a *ServerController) getConfigJson(c *gin.Context) { jsonObj(c, configJson, nil) } +// getDb downloads the database file. func (a *ServerController) getDb(c *gin.Context) { db, err := a.serverService.GetDb() if err != nil { @@ -238,6 +254,7 @@ func isValidFilename(filename string) bool { return filenameRegex.MatchString(filename) } +// importDB imports a database file and restarts the Xray service. func (a *ServerController) importDB(c *gin.Context) { // Get the file from the request body file, _, err := c.Request.FormFile("db") @@ -258,6 +275,7 @@ func (a *ServerController) importDB(c *gin.Context) { jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil) } +// getNewX25519Cert generates a new X25519 certificate. func (a *ServerController) getNewX25519Cert(c *gin.Context) { cert, err := a.serverService.GetNewX25519Cert() if err != nil { @@ -267,6 +285,7 @@ func (a *ServerController) getNewX25519Cert(c *gin.Context) { jsonObj(c, cert, nil) } +// getNewmldsa65 generates a new ML-DSA-65 key. func (a *ServerController) getNewmldsa65(c *gin.Context) { cert, err := a.serverService.GetNewmldsa65() if err != nil { @@ -276,6 +295,7 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) { jsonObj(c, cert, nil) } +// getNewEchCert generates a new ECH certificate for the given SNI. func (a *ServerController) getNewEchCert(c *gin.Context) { sni := c.PostForm("sni") cert, err := a.serverService.GetNewEchCert(sni) @@ -286,6 +306,7 @@ func (a *ServerController) getNewEchCert(c *gin.Context) { jsonObj(c, cert, nil) } +// getNewVlessEnc generates a new VLESS encryption key. func (a *ServerController) getNewVlessEnc(c *gin.Context) { out, err := a.serverService.GetNewVlessEnc() if err != nil { @@ -295,6 +316,7 @@ func (a *ServerController) getNewVlessEnc(c *gin.Context) { jsonObj(c, out, nil) } +// getNewUUID generates a new UUID. func (a *ServerController) getNewUUID(c *gin.Context) { uuidResp, err := a.serverService.GetNewUUID() if err != nil { @@ -305,6 +327,7 @@ func (a *ServerController) getNewUUID(c *gin.Context) { jsonObj(c, uuidResp, nil) } +// getNewmlkem768 generates a new ML-KEM-768 key. func (a *ServerController) getNewmlkem768(c *gin.Context) { out, err := a.serverService.GetNewmlkem768() if err != nil { diff --git a/web/controller/setting.go b/web/controller/setting.go index 46f760af..fc5486bc 100644 --- a/web/controller/setting.go +++ b/web/controller/setting.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" ) +// updateUserForm represents the form for updating user credentials. type updateUserForm struct { OldUsername string `json:"oldUsername" form:"oldUsername"` OldPassword string `json:"oldPassword" form:"oldPassword"` @@ -19,18 +20,21 @@ type updateUserForm struct { NewPassword string `json:"newPassword" form:"newPassword"` } +// SettingController handles settings and user management operations. type SettingController struct { settingService service.SettingService userService service.UserService panelService service.PanelService } +// NewSettingController creates a new SettingController and initializes its routes. func NewSettingController(g *gin.RouterGroup) *SettingController { a := &SettingController{} a.initRouter(g) return a } +// initRouter sets up the routes for settings management. func (a *SettingController) initRouter(g *gin.RouterGroup) { g = g.Group("/setting") @@ -42,6 +46,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) { g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) } +// getAllSetting retrieves all current settings. func (a *SettingController) getAllSetting(c *gin.Context) { allSetting, err := a.settingService.GetAllSetting() if err != nil { @@ -51,6 +56,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) { jsonObj(c, allSetting, nil) } +// getDefaultSettings retrieves the default settings based on the host. func (a *SettingController) getDefaultSettings(c *gin.Context) { result, err := a.settingService.GetDefaultSettings(c.Request.Host) if err != nil { @@ -60,6 +66,7 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) { jsonObj(c, result, nil) } +// updateSetting updates all settings with the provided data. func (a *SettingController) updateSetting(c *gin.Context) { allSetting := &entity.AllSetting{} err := c.ShouldBind(allSetting) @@ -71,6 +78,7 @@ func (a *SettingController) updateSetting(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) } +// updateUser updates the current user's username and password. func (a *SettingController) updateUser(c *gin.Context) { form := &updateUserForm{} err := c.ShouldBind(form) @@ -96,11 +104,13 @@ func (a *SettingController) updateUser(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) } +// restartPanel restarts the panel service after a delay. func (a *SettingController) restartPanel(c *gin.Context) { err := a.panelService.RestartPanel(time.Second * 3) jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err) } +// getDefaultXrayConfig retrieves the default Xray configuration. func (a *SettingController) getDefaultXrayConfig(c *gin.Context) { defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig() if err != nil { diff --git a/web/controller/util.go b/web/controller/util.go index e180b1fe..b11203bd 100644 --- a/web/controller/util.go +++ b/web/controller/util.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" ) +// getRemoteIp extracts the real IP address from the request headers or remote address. func getRemoteIp(c *gin.Context) string { value := c.GetHeader("X-Real-IP") if value != "" { @@ -27,14 +28,17 @@ func getRemoteIp(c *gin.Context) string { return ip } +// jsonMsg sends a JSON response with a message and error status. func jsonMsg(c *gin.Context, msg string, err error) { jsonMsgObj(c, msg, nil, err) } +// jsonObj sends a JSON response with an object and error status. func jsonObj(c *gin.Context, obj any, err error) { jsonMsgObj(c, "", obj, err) } +// jsonMsgObj sends a JSON response with a message, object, and error status. func jsonMsgObj(c *gin.Context, msg string, obj any, err error) { m := entity.Msg{ Obj: obj, @@ -52,6 +56,7 @@ func jsonMsgObj(c *gin.Context, msg string, obj any, err error) { c.JSON(http.StatusOK, m) } +// pureJsonMsg sends a pure JSON message response with custom status code. func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) { c.JSON(statusCode, entity.Msg{ Success: success, @@ -59,6 +64,7 @@ func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) { }) } +// html renders an HTML template with the provided data and title. func html(c *gin.Context, name string, title string, data gin.H) { if data == nil { data = gin.H{} @@ -81,6 +87,7 @@ func html(c *gin.Context, name string, title string, data gin.H) { c.HTML(http.StatusOK, name, getContext(data)) } +// getContext adds version and other context data to the provided gin.H. func getContext(h gin.H) gin.H { a := gin.H{ "cur_ver": config.GetVersion(), @@ -91,6 +98,7 @@ func getContext(h gin.H) gin.H { return a } +// isAjax checks if the request is an AJAX request. func isAjax(c *gin.Context) bool { return c.GetHeader("X-Requested-With") == "XMLHttpRequest" } diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go index bdbb370c..b78925f0 100644 --- a/web/controller/xray_setting.go +++ b/web/controller/xray_setting.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" ) +// XraySettingController handles Xray configuration and settings operations. type XraySettingController struct { XraySettingService service.XraySettingService SettingService service.SettingService @@ -15,12 +16,14 @@ type XraySettingController struct { WarpService service.WarpService } +// NewXraySettingController creates a new XraySettingController and initializes its routes. func NewXraySettingController(g *gin.RouterGroup) *XraySettingController { a := &XraySettingController{} a.initRouter(g) return a } +// initRouter sets up the routes for Xray settings management. func (a *XraySettingController) initRouter(g *gin.RouterGroup) { g = g.Group("/xray") g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) @@ -33,6 +36,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) { g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) } +// getXraySetting retrieves the Xray configuration template and inbound tags. func (a *XraySettingController) getXraySetting(c *gin.Context) { xraySetting, err := a.SettingService.GetXrayConfigTemplate() if err != nil { @@ -48,12 +52,14 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) { jsonObj(c, xrayResponse, nil) } +// updateSetting updates the Xray configuration settings. func (a *XraySettingController) updateSetting(c *gin.Context) { xraySetting := c.PostForm("xraySetting") err := a.XraySettingService.SaveXraySetting(xraySetting) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) } +// getDefaultXrayConfig retrieves the default Xray configuration. func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) { defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig() if err != nil { @@ -63,10 +69,12 @@ func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) { jsonObj(c, defaultJsonConfig, nil) } +// getXrayResult retrieves the current Xray service result. func (a *XraySettingController) getXrayResult(c *gin.Context) { jsonObj(c, a.XrayService.GetXrayResult(), nil) } +// warp handles Warp-related operations based on the action parameter. func (a *XraySettingController) warp(c *gin.Context) { action := c.Param("action") var resp string @@ -90,6 +98,7 @@ func (a *XraySettingController) warp(c *gin.Context) { jsonObj(c, resp, err) } +// getOutboundsTraffic retrieves the traffic statistics for outbounds. func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) { outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic() if err != nil { @@ -99,6 +108,7 @@ func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) { jsonObj(c, outboundsTraffic, nil) } +// resetOutboundsTraffic resets the traffic statistics for the specified outbound tag. func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) { tag := c.PostForm("tag") err := a.OutboundService.ResetOutboundTraffic(tag) diff --git a/web/controller/xui.go b/web/controller/xui.go index 1afbc427..ba415ac9 100644 --- a/web/controller/xui.go +++ b/web/controller/xui.go @@ -4,6 +4,7 @@ import ( "github.com/gin-gonic/gin" ) +// XUIController is the main controller for the X-UI panel, managing sub-controllers. type XUIController struct { BaseController @@ -13,12 +14,14 @@ type XUIController struct { xraySettingController *XraySettingController } +// NewXUIController creates a new XUIController and initializes its routes. func NewXUIController(g *gin.RouterGroup) *XUIController { a := &XUIController{} a.initRouter(g) return a } +// initRouter sets up the main panel routes and initializes sub-controllers. func (a *XUIController) initRouter(g *gin.RouterGroup) { g = g.Group("/panel") g.Use(a.checkLogin) @@ -34,18 +37,22 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) { a.xraySettingController = NewXraySettingController(g) } +// index renders the main panel index page. func (a *XUIController) index(c *gin.Context) { html(c, "index.html", "pages.index.title", nil) } +// inbounds renders the inbounds management page. func (a *XUIController) inbounds(c *gin.Context) { html(c, "inbounds.html", "pages.inbounds.title", nil) } +// settings renders the settings management page. func (a *XUIController) settings(c *gin.Context) { html(c, "settings.html", "pages.settings.title", nil) } +// xraySettings renders the Xray settings page. func (a *XUIController) xraySettings(c *gin.Context) { html(c, "xray.html", "pages.xray.title", nil) } diff --git a/web/entity/entity.go b/web/entity/entity.go index 41d19d3b..adb60972 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -1,3 +1,4 @@ +// Package entity defines data structures and entities used by the web layer of the 3x-ui panel. package entity import ( @@ -10,61 +11,73 @@ import ( "github.com/mhsanaei/3x-ui/v2/util/common" ) +// Msg represents a standard API response message with success status, message text, and optional data object. type Msg struct { - Success bool `json:"success"` - Msg string `json:"msg"` - Obj any `json:"obj"` + Success bool `json:"success"` // Indicates if the operation was successful + Msg string `json:"msg"` // Response message text + Obj any `json:"obj"` // Optional data object } +// AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings. type AllSetting struct { - WebListen string `json:"webListen" form:"webListen"` - WebDomain string `json:"webDomain" form:"webDomain"` - WebPort int `json:"webPort" form:"webPort"` - WebCertFile string `json:"webCertFile" form:"webCertFile"` - WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` - WebBasePath string `json:"webBasePath" form:"webBasePath"` - SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` - PageSize int `json:"pageSize" form:"pageSize"` - ExpireDiff int `json:"expireDiff" form:"expireDiff"` - TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` - RemarkModel string `json:"remarkModel" form:"remarkModel"` - TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` - TgBotToken string `json:"tgBotToken" form:"tgBotToken"` - TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` - TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` - TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` - TgRunTime string `json:"tgRunTime" form:"tgRunTime"` - TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` - TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` - TgCpu int `json:"tgCpu" form:"tgCpu"` - TgLang string `json:"tgLang" form:"tgLang"` - TimeLocation string `json:"timeLocation" form:"timeLocation"` - TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` - TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` - SubEnable bool `json:"subEnable" form:"subEnable"` - SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` - SubTitle string `json:"subTitle" form:"subTitle"` - SubListen string `json:"subListen" form:"subListen"` - SubPort int `json:"subPort" form:"subPort"` - SubPath string `json:"subPath" form:"subPath"` - SubDomain string `json:"subDomain" form:"subDomain"` - SubCertFile string `json:"subCertFile" form:"subCertFile"` - SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` - SubUpdates int `json:"subUpdates" form:"subUpdates"` - ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` - ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"` - SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` - SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` - SubURI string `json:"subURI" form:"subURI"` - SubJsonPath string `json:"subJsonPath" form:"subJsonPath"` - SubJsonURI string `json:"subJsonURI" form:"subJsonURI"` - SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` - SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` - SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` - SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` - Datepicker string `json:"datepicker" form:"datepicker"` + // Web server settings + WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address + WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation + WebPort int `json:"webPort" form:"webPort"` // Web server port number + WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server + WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server + WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs + SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` // Session maximum age in minutes + + // UI settings + PageSize int `json:"pageSize" form:"pageSize"` // Number of items per page in lists + ExpireDiff int `json:"expireDiff" form:"expireDiff"` // Expiration warning threshold in days + TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` // Traffic warning threshold percentage + RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds + Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format + + // Telegram bot settings + TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications + TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token + TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot + TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot + TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications + TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications + TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram + TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications + TgCpu int `json:"tgCpu" form:"tgCpu"` // CPU usage threshold for alerts + TgLang string `json:"tgLang" form:"tgLang"` // Telegram bot language + + // Security settings + TimeLocation string `json:"timeLocation" form:"timeLocation"` // Time zone location + TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` // Enable two-factor authentication + TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` // Two-factor authentication token + + // Subscription server settings + SubEnable bool `json:"subEnable" form:"subEnable"` // Enable subscription server + SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` // Enable JSON subscription endpoint + SubTitle string `json:"subTitle" form:"subTitle"` // Subscription title + SubListen string `json:"subListen" form:"subListen"` // Subscription server listen IP + SubPort int `json:"subPort" form:"subPort"` // Subscription server port + SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs + SubDomain string `json:"subDomain" form:"subDomain"` // Domain for subscription server validation + SubCertFile string `json:"subCertFile" form:"subCertFile"` // SSL certificate file for subscription server + SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` // SSL private key file for subscription server + SubUpdates int `json:"subUpdates" form:"subUpdates"` // Subscription update interval in minutes + ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` // Enable external traffic reporting + ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"` // URI for external traffic reporting + SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` // Encrypt subscription responses + SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` // Show client information in subscriptions + SubURI string `json:"subURI" form:"subURI"` // Subscription server URI + SubJsonPath string `json:"subJsonPath" form:"subJsonPath"` // Path for JSON subscription endpoint + SubJsonURI string `json:"subJsonURI" form:"subJsonURI"` // JSON subscription server URI + SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration + SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration + SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration + SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` // JSON subscription routing rules } +// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values. func (s *AllSetting) CheckValid() error { if s.WebListen != "" { ip := net.ParseIP(s.WebListen) diff --git a/web/global/global.go b/web/global/global.go index e92c375b..025fa081 100644 --- a/web/global/global.go +++ b/web/global/global.go @@ -1,3 +1,4 @@ +// Package global provides global variables and interfaces for accessing web and subscription servers. package global import ( @@ -12,27 +13,33 @@ var ( subServer SubServer ) +// WebServer interface defines methods for accessing the web server instance. type WebServer interface { - GetCron() *cron.Cron - GetCtx() context.Context + GetCron() *cron.Cron // Get the cron scheduler + GetCtx() context.Context // Get the server context } +// SubServer interface defines methods for accessing the subscription server instance. type SubServer interface { - GetCtx() context.Context + GetCtx() context.Context // Get the server context } +// SetWebServer sets the global web server instance. func SetWebServer(s WebServer) { webServer = s } +// GetWebServer returns the global web server instance. func GetWebServer() WebServer { return webServer } +// SetSubServer sets the global subscription server instance. func SetSubServer(s SubServer) { subServer = s } +// GetSubServer returns the global subscription server instance. func GetSubServer() SubServer { return subServer } diff --git a/web/global/hashStorage.go b/web/global/hashStorage.go index 5d8135ee..962493f7 100644 --- a/web/global/hashStorage.go +++ b/web/global/hashStorage.go @@ -8,18 +8,21 @@ import ( "time" ) +// HashEntry represents a stored hash entry with its value and timestamp. type HashEntry struct { - Hash string - Value string - Timestamp time.Time + Hash string // MD5 hash string + Value string // Original value + Timestamp time.Time // Time when the hash was created } +// HashStorage provides thread-safe storage for hash-value pairs with expiration. type HashStorage struct { sync.RWMutex - Data map[string]HashEntry - Expiration time.Duration + Data map[string]HashEntry // Map of hash to entry + Expiration time.Duration // Expiration duration for entries } +// NewHashStorage creates a new HashStorage instance with the specified expiration duration. func NewHashStorage(expiration time.Duration) *HashStorage { return &HashStorage{ Data: make(map[string]HashEntry), @@ -27,6 +30,7 @@ func NewHashStorage(expiration time.Duration) *HashStorage { } } +// SaveHash generates an MD5 hash for the given query string and stores it with a timestamp. func (h *HashStorage) SaveHash(query string) string { h.Lock() defer h.Unlock() @@ -45,6 +49,7 @@ func (h *HashStorage) SaveHash(query string) string { return md5HashString } +// GetValue retrieves the original value for the given hash, returning true if found. func (h *HashStorage) GetValue(hash string) (string, bool) { h.RLock() defer h.RUnlock() @@ -54,11 +59,13 @@ func (h *HashStorage) GetValue(hash string) (string, bool) { return entry.Value, exists } +// IsMD5 checks if the given string is a valid 32-character MD5 hash. func (h *HashStorage) IsMD5(hash string) bool { match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash) return match } +// RemoveExpiredHashes removes all hash entries that have exceeded the expiration duration. func (h *HashStorage) RemoveExpiredHashes() { h.Lock() defer h.Unlock() @@ -72,6 +79,7 @@ func (h *HashStorage) RemoveExpiredHashes() { } } +// Reset clears all stored hash entries. func (h *HashStorage) Reset() { h.Lock() defer h.Unlock() diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go index 7704e10d..e783a6df 100644 --- a/web/job/check_client_ip_job.go +++ b/web/job/check_client_ip_job.go @@ -18,6 +18,7 @@ import ( "github.com/mhsanaei/3x-ui/v2/xray" ) +// CheckClientIpJob monitors client IP addresses from access logs and manages IP blocking based on configured limits. type CheckClientIpJob struct { lastClear int64 disAllowedIps []string @@ -25,6 +26,7 @@ type CheckClientIpJob struct { var job *CheckClientIpJob +// NewCheckClientIpJob creates a new client IP monitoring job instance. func NewCheckClientIpJob() *CheckClientIpJob { job = new(CheckClientIpJob) return job diff --git a/web/job/check_cpu_usage.go b/web/job/check_cpu_usage.go index 5cb9a21e..2ea87747 100644 --- a/web/job/check_cpu_usage.go +++ b/web/job/check_cpu_usage.go @@ -9,16 +9,18 @@ import ( "github.com/shirou/gopsutil/v4/cpu" ) +// CheckCpuJob monitors CPU usage and sends Telegram notifications when usage exceeds the configured threshold. type CheckCpuJob struct { tgbotService service.Tgbot settingService service.SettingService } +// NewCheckCpuJob creates a new CPU monitoring job instance. func NewCheckCpuJob() *CheckCpuJob { return new(CheckCpuJob) } -// Here run is a interface method of Job interface +// Run checks CPU usage over the last minute and sends a Telegram alert if it exceeds the threshold. func (j *CheckCpuJob) Run() { threshold, _ := j.settingService.GetTgCpu() diff --git a/web/job/check_hash_storage.go b/web/job/check_hash_storage.go index 5f826d63..2112079e 100644 --- a/web/job/check_hash_storage.go +++ b/web/job/check_hash_storage.go @@ -4,15 +4,17 @@ import ( "github.com/mhsanaei/3x-ui/v2/web/service" ) +// CheckHashStorageJob periodically cleans up expired hash entries from the Telegram bot's hash storage. type CheckHashStorageJob struct { tgbotService service.Tgbot } +// NewCheckHashStorageJob creates a new hash storage cleanup job instance. func NewCheckHashStorageJob() *CheckHashStorageJob { return new(CheckHashStorageJob) } -// Here Run is an interface method of the Job interface +// Run removes expired hash entries from the Telegram bot's hash storage. func (j *CheckHashStorageJob) Run() { // Remove expired hashes from storage j.tgbotService.GetHashStorage().RemoveExpiredHashes() diff --git a/web/job/check_xray_running_job.go b/web/job/check_xray_running_job.go index 8f5f0889..5b53b0c6 100644 --- a/web/job/check_xray_running_job.go +++ b/web/job/check_xray_running_job.go @@ -1,3 +1,5 @@ +// Package job provides background job implementations for the 3x-ui web panel, +// including traffic monitoring, system checks, and periodic maintenance tasks. package job import ( @@ -5,16 +7,18 @@ import ( "github.com/mhsanaei/3x-ui/v2/web/service" ) +// CheckXrayRunningJob monitors Xray process health and restarts it if it crashes. type CheckXrayRunningJob struct { xrayService service.XrayService - - checkTime int + checkTime int } +// NewCheckXrayRunningJob creates a new Xray health check job instance. func NewCheckXrayRunningJob() *CheckXrayRunningJob { return new(CheckXrayRunningJob) } +// Run checks if Xray has crashed and restarts it after confirming it's down for 2 consecutive checks. func (j *CheckXrayRunningJob) Run() { if !j.xrayService.DidXrayCrash() { j.checkTime = 0 diff --git a/web/job/clear_logs_job.go b/web/job/clear_logs_job.go index 70e03688..85e0d64a 100644 --- a/web/job/clear_logs_job.go +++ b/web/job/clear_logs_job.go @@ -9,8 +9,10 @@ import ( "github.com/mhsanaei/3x-ui/v2/xray" ) +// ClearLogsJob clears old log files to prevent disk space issues. type ClearLogsJob struct{} +// NewClearLogsJob creates a new log cleanup job instance. func NewClearLogsJob() *ClearLogsJob { return new(ClearLogsJob) } diff --git a/web/job/periodic_traffic_reset_job.go b/web/job/periodic_traffic_reset_job.go index 65aefb9d..6a370a51 100644 --- a/web/job/periodic_traffic_reset_job.go +++ b/web/job/periodic_traffic_reset_job.go @@ -5,19 +5,23 @@ import ( "github.com/mhsanaei/3x-ui/v2/web/service" ) +// Period represents the time period for traffic resets. type Period string +// PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period. type PeriodicTrafficResetJob struct { inboundService service.InboundService period Period } +// NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period. func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob { return &PeriodicTrafficResetJob{ period: period, } } +// Run resets traffic statistics for all inbounds that match the configured reset period. func (j *PeriodicTrafficResetJob) Run() { inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period)) if err != nil { diff --git a/web/job/stats_notify_job.go b/web/job/stats_notify_job.go index 0d0907e0..b93f05cb 100644 --- a/web/job/stats_notify_job.go +++ b/web/job/stats_notify_job.go @@ -4,23 +4,26 @@ import ( "github.com/mhsanaei/3x-ui/v2/web/service" ) +// LoginStatus represents the status of a login attempt. type LoginStatus byte const ( - LoginSuccess LoginStatus = 1 - LoginFail LoginStatus = 0 + LoginSuccess LoginStatus = 1 // Successful login + LoginFail LoginStatus = 0 // Failed login attempt ) +// StatsNotifyJob sends periodic statistics reports via Telegram bot. type StatsNotifyJob struct { xrayService service.XrayService tgbotService service.Tgbot } +// NewStatsNotifyJob creates a new statistics notification job instance. func NewStatsNotifyJob() *StatsNotifyJob { return new(StatsNotifyJob) } -// Here run is a interface method of Job interface +// Run sends a statistics report via Telegram bot if Xray is running. func (j *StatsNotifyJob) Run() { if !j.xrayService.IsXrayRunning() { return diff --git a/web/job/xray_traffic_job.go b/web/job/xray_traffic_job.go index 8ba5a9f9..a9affb4b 100644 --- a/web/job/xray_traffic_job.go +++ b/web/job/xray_traffic_job.go @@ -10,6 +10,7 @@ import ( "github.com/valyala/fasthttp" ) +// XrayTrafficJob collects and processes traffic statistics from Xray, updating the database and optionally informing external APIs. type XrayTrafficJob struct { settingService service.SettingService xrayService service.XrayService @@ -17,10 +18,12 @@ type XrayTrafficJob struct { outboundService service.OutboundService } +// NewXrayTrafficJob creates a new traffic collection job instance. func NewXrayTrafficJob() *XrayTrafficJob { return new(XrayTrafficJob) } +// Run collects traffic statistics from Xray and updates the database, triggering restart if needed. func (j *XrayTrafficJob) Run() { if !j.xrayService.IsXrayRunning() { return diff --git a/web/locale/locale.go b/web/locale/locale.go index b4cb9464..c469911a 100644 --- a/web/locale/locale.go +++ b/web/locale/locale.go @@ -1,3 +1,5 @@ +// Package locale provides internationalization (i18n) support for the 3x-ui web panel, +// including translation loading, localization, and middleware for web and bot interfaces. package locale import ( @@ -20,17 +22,20 @@ var ( LocalizerBot *i18n.Localizer ) +// I18nType represents the type of interface for internationalization. type I18nType string const ( - Bot I18nType = "bot" - Web I18nType = "web" + Bot I18nType = "bot" // Bot interface type + Web I18nType = "web" // Web interface type ) +// SettingService interface defines methods for accessing locale settings. type SettingService interface { GetTgLang() (string, error) } +// InitLocalizer initializes the internationalization system with embedded translation files. func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { // set default bundle to english i18nBundle = i18n.NewBundle(language.MustParse("en-US")) @@ -49,6 +54,7 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { return nil } +// createTemplateData creates a template data map from parameters with optional separator. func createTemplateData(params []string, separator ...string) map[string]any { var sep string = "==" if len(separator) > 0 { @@ -64,6 +70,9 @@ func createTemplateData(params []string, separator ...string) map[string]any { return templateData } +// I18n retrieves a localized message for the given key and type. +// It supports both bot and web contexts, with optional template parameters. +// Returns the localized message or an empty string if localization fails. func I18n(i18nType I18nType, key string, params ...string) string { var localizer *i18n.Localizer @@ -96,6 +105,7 @@ func I18n(i18nType I18nType, key string, params ...string) string { return msg } +// initTGBotLocalizer initializes the bot localizer with the configured language. func initTGBotLocalizer(settingService SettingService) error { botLang, err := settingService.GetTgLang() if err != nil { @@ -106,6 +116,10 @@ func initTGBotLocalizer(settingService SettingService) error { return nil } +// LocalizerMiddleware returns a Gin middleware that sets up localization for web requests. +// It determines the user's language from cookies or Accept-Language header, +// creates a localizer instance, and stores it in the Gin context for use in handlers. +// Also provides the I18n function in the context for template rendering. func LocalizerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Ensure bundle is initialized so creating a Localizer won't panic @@ -152,6 +166,7 @@ func loadTranslationsFromDisk(bundle *i18n.Bundle) error { }) } +// parseTranslationFiles parses embedded translation files and adds them to the i18n bundle. func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error { err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error { diff --git a/web/middleware/domainValidator.go b/web/middleware/domainValidator.go index c94130c0..ae4793cb 100644 --- a/web/middleware/domainValidator.go +++ b/web/middleware/domainValidator.go @@ -1,3 +1,5 @@ +// Package middleware provides HTTP middleware functions for the 3x-ui web panel, +// including domain validation and URL redirection utilities. package middleware import ( @@ -8,6 +10,10 @@ import ( "github.com/gin-gonic/gin" ) +// DomainValidatorMiddleware returns a Gin middleware that validates the request domain. +// It extracts the host from the request, strips any port number, and compares it +// against the configured domain. Requests from unauthorized domains are rejected +// with HTTP 403 Forbidden status. func DomainValidatorMiddleware(domain string) gin.HandlerFunc { return func(c *gin.Context) { host := c.Request.Host diff --git a/web/middleware/redirect.go b/web/middleware/redirect.go index e3dc8ada..966d897c 100644 --- a/web/middleware/redirect.go +++ b/web/middleware/redirect.go @@ -7,6 +7,9 @@ import ( "github.com/gin-gonic/gin" ) +// RedirectMiddleware returns a Gin middleware that handles URL redirections. +// It provides backward compatibility by redirecting old '/xui' paths to new '/panel' paths, +// including API endpoints. The middleware performs permanent redirects (301) for SEO purposes. func RedirectMiddleware(basePath string) gin.HandlerFunc { return func(c *gin.Context) { // Redirect from old '/xui' path to '/panel' diff --git a/web/network/auto_https_conn.go b/web/network/auto_https_conn.go index d1a9d521..aa0e9dea 100644 --- a/web/network/auto_https_conn.go +++ b/web/network/auto_https_conn.go @@ -1,3 +1,5 @@ +// Package network provides network utilities for the 3x-ui web panel, +// including automatic HTTP to HTTPS redirection functionality. package network import ( @@ -9,6 +11,9 @@ import ( "sync" ) +// AutoHttpsConn wraps a net.Conn to provide automatic HTTP to HTTPS redirection. +// It intercepts the first read to detect HTTP requests and responds with a 307 redirect +// to the HTTPS equivalent URL. Subsequent reads work normally for HTTPS connections. type AutoHttpsConn struct { net.Conn @@ -18,6 +23,8 @@ type AutoHttpsConn struct { readRequestOnce sync.Once } +// NewAutoHttpsConn creates a new AutoHttpsConn that wraps the given connection. +// It enables automatic redirection of HTTP requests to HTTPS. func NewAutoHttpsConn(conn net.Conn) net.Conn { return &AutoHttpsConn{ Conn: conn, @@ -49,6 +56,9 @@ func (c *AutoHttpsConn) readRequest() bool { return true } +// Read implements the net.Conn Read method with automatic HTTPS redirection. +// On the first read, it checks if the request is HTTP and redirects to HTTPS if so. +// Subsequent reads work normally. func (c *AutoHttpsConn) Read(buf []byte) (int, error) { c.readRequestOnce.Do(func() { c.readRequest() diff --git a/web/network/auto_https_listener.go b/web/network/auto_https_listener.go index 26614696..32dc307d 100644 --- a/web/network/auto_https_listener.go +++ b/web/network/auto_https_listener.go @@ -2,16 +2,22 @@ package network import "net" +// AutoHttpsListener wraps a net.Listener to provide automatic HTTPS redirection. +// It returns AutoHttpsConn connections that handle HTTP to HTTPS redirection. type AutoHttpsListener struct { net.Listener } +// NewAutoHttpsListener creates a new AutoHttpsListener that wraps the given listener. +// It enables automatic redirection of HTTP requests to HTTPS for all accepted connections. func NewAutoHttpsListener(listener net.Listener) net.Listener { return &AutoHttpsListener{ Listener: listener, } } +// Accept implements the net.Listener Accept method. +// It accepts connections and wraps them with AutoHttpsConn for HTTPS redirection. func (l *AutoHttpsListener) Accept() (net.Conn, error) { conn, err := l.Listener.Accept() if err != nil { diff --git a/web/service/inbound.go b/web/service/inbound.go index 414d5945..5c6083ee 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1,3 +1,5 @@ +// Package service provides business logic services for the 3x-ui web panel, +// including inbound/outbound management, user administration, settings, and Xray integration. package service import ( @@ -17,10 +19,15 @@ import ( "gorm.io/gorm" ) +// InboundService provides business logic for managing Xray inbound configurations. +// It handles CRUD operations for inbounds, client management, traffic monitoring, +// and integration with the Xray API for real-time updates. type InboundService struct { xrayApi xray.XrayAPI } +// GetInbounds retrieves all inbounds for a specific user. +// Returns a slice of inbound models with their associated client statistics. func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound @@ -31,6 +38,8 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { return inbounds, nil } +// GetAllInbounds retrieves all inbounds from the database. +// Returns a slice of all inbound models with their associated client statistics. func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound @@ -163,6 +172,10 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri return "", nil } +// AddInbound creates a new inbound configuration. +// It validates port uniqueness, client email uniqueness, and required fields, +// then saves the inbound to the database and optionally adds it to the running Xray instance. +// Returns the created inbound, whether Xray needs restart, and any error. func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) { exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0) if err != nil { @@ -269,6 +282,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo return inbound, needRestart, err } +// DelInbound deletes an inbound configuration by ID. +// It removes the inbound from the database and the running Xray instance if active. +// Returns whether Xray needs restart and any error. func (s *InboundService) DelInbound(id int) (bool, error) { db := database.GetDB() @@ -322,6 +338,9 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) { return inbound, nil } +// UpdateInbound modifies an existing inbound configuration. +// It validates changes, updates the database, and syncs with the running Xray instance. +// Returns the updated inbound, whether Xray needs restart, and any error. func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) { exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id) if err != nil { diff --git a/web/service/outbound.go b/web/service/outbound.go index 94a8f0b3..530d12eb 100644 --- a/web/service/outbound.go +++ b/web/service/outbound.go @@ -9,6 +9,8 @@ import ( "gorm.io/gorm" ) +// OutboundService provides business logic for managing Xray outbound configurations. +// It handles outbound traffic monitoring and statistics. type OutboundService struct{} func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { diff --git a/web/service/panel.go b/web/service/panel.go index 72576299..e4fb0c68 100644 --- a/web/service/panel.go +++ b/web/service/panel.go @@ -8,6 +8,8 @@ import ( "github.com/mhsanaei/3x-ui/v2/logger" ) +// PanelService provides business logic for panel management operations. +// It handles panel restart, updates, and system-level panel controls. type PanelService struct{} func (s *PanelService) RestartPanel(delay time.Duration) error { diff --git a/web/service/server.go b/web/service/server.go index 03199b50..9fe42e2c 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -35,14 +35,18 @@ import ( "github.com/shirou/gopsutil/v4/net" ) +// ProcessState represents the current state of a system process. type ProcessState string +// Process state constants const ( - Running ProcessState = "running" - Stop ProcessState = "stop" - Error ProcessState = "error" + Running ProcessState = "running" // Process is running normally + Stop ProcessState = "stop" // Process is stopped + Error ProcessState = "error" // Process is in error state ) +// Status represents comprehensive system and application status information. +// It includes CPU, memory, disk, network statistics, and Xray process status. type Status struct { T time.Time `json:"-"` Cpu float64 `json:"cpu"` @@ -89,10 +93,13 @@ type Status struct { } `json:"appStats"` } +// Release represents information about a software release from GitHub. type Release struct { - TagName string `json:"tag_name"` + TagName string `json:"tag_name"` // The tag name of the release } +// ServerService provides business logic for server monitoring and management. +// It handles system status collection, IP detection, and application statistics. type ServerService struct { xrayService XrayService inboundService InboundService diff --git a/web/service/setting.go b/web/service/setting.go index 7046464a..530a6344 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -75,6 +75,8 @@ var defaultValueMap = map[string]string{ "externalTrafficInformURI": "", } +// SettingService provides business logic for application settings management. +// It handles configuration storage, retrieval, and validation for all system settings. type SettingService struct{} func (s *SettingService) GetDefaultJsonConfig() (any, error) { diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 44e4af28..e575bb28 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -65,14 +65,18 @@ var ( var userStates = make(map[int64]string) +// LoginStatus represents the result of a login attempt. type LoginStatus byte +// Login status constants const ( - LoginSuccess LoginStatus = 1 - LoginFail LoginStatus = 0 - EmptyTelegramUserID = int64(0) + LoginSuccess LoginStatus = 1 // Login was successful + LoginFail LoginStatus = 0 // Login failed + EmptyTelegramUserID = int64(0) // Default value for empty Telegram user ID ) +// Tgbot provides business logic for Telegram bot integration. +// It handles bot commands, user interactions, and status reporting via Telegram. type Tgbot struct { inboundService InboundService settingService SettingService @@ -81,18 +85,22 @@ type Tgbot struct { lastStatus *Status } +// NewTgbot creates a new Tgbot instance. func (t *Tgbot) NewTgbot() *Tgbot { return new(Tgbot) } +// I18nBot retrieves a localized message for the bot interface. func (t *Tgbot) I18nBot(name string, params ...string) string { return locale.I18n(locale.Bot, name, params...) } +// GetHashStorage returns the hash storage instance for callback queries. func (t *Tgbot) GetHashStorage() *global.HashStorage { return hashStorage } +// Start initializes and starts the Telegram bot with the provided translation files. func (t *Tgbot) Start(i18nFS embed.FS) error { // Initialize localizer err := locale.InitLocalizer(i18nFS, &t.settingService) @@ -173,6 +181,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { return nil } +// NewBot creates a new Telegram bot instance with optional proxy and API server settings. func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) { if proxyUrl == "" && apiServerUrl == "" { return telego.NewBot(token) @@ -209,10 +218,12 @@ func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*tel return telego.NewBot(token, telego.WithAPIServer(apiServerUrl)) } +// IsRunning checks if the Telegram bot is currently running. func (t *Tgbot) IsRunning() bool { return isRunning } +// SetHostname sets the hostname for the bot. func (t *Tgbot) SetHostname() { host, err := os.Hostname() if err != nil { @@ -223,6 +234,7 @@ func (t *Tgbot) SetHostname() { hostname = host } +// Stop stops the Telegram bot and cleans up resources. func (t *Tgbot) Stop() { if botHandler != nil { botHandler.Stop() @@ -232,6 +244,7 @@ func (t *Tgbot) Stop() { adminIds = nil } +// encodeQuery encodes the query string if it's longer than 64 characters. func (t *Tgbot) encodeQuery(query string) string { // NOTE: we only need to hash for more than 64 chars if len(query) <= 64 { @@ -241,6 +254,7 @@ func (t *Tgbot) encodeQuery(query string) string { return hashStorage.SaveHash(query) } +// decodeQuery decodes a hashed query string back to its original form. func (t *Tgbot) decodeQuery(query string) (string, error) { if !hashStorage.IsMD5(query) { return query, nil @@ -254,6 +268,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) { return decoded, nil } +// OnReceive starts the message receiving loop for the Telegram bot. func (t *Tgbot) OnReceive() { params := telego.GetUpdatesParams{ Timeout: 10, @@ -430,6 +445,7 @@ func (t *Tgbot) OnReceive() { botHandler.Start() } +// answerCommand processes incoming command messages from Telegram users. func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) { msg, onlyMessage := "", false @@ -505,7 +521,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo } } -// Helper function to send the message based on onlyMessage flag. +// sendResponse sends the response message based on the onlyMessage flag. func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) { if onlyMessage { t.SendMsgToTgbot(chatId, msg) @@ -514,6 +530,7 @@ func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool } } +// randomLowerAndNum generates a random string of lowercase letters and numbers. func (t *Tgbot) randomLowerAndNum(length int) string { charset := "abcdefghijklmnopqrstuvwxyz0123456789" bytes := make([]byte, length) @@ -524,6 +541,7 @@ func (t *Tgbot) randomLowerAndNum(length int) string { return string(bytes) } +// randomShadowSocksPassword generates a random password for Shadowsocks. func (t *Tgbot) randomShadowSocksPassword() string { array := make([]byte, 32) _, err := rand.Read(array) @@ -533,6 +551,7 @@ func (t *Tgbot) randomShadowSocksPassword() string { return base64.StdEncoding.EncodeToString(array) } +// answerCallback processes callback queries from inline keyboards. func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) { chatId := callbackQuery.Message.GetChat().ID @@ -1815,6 +1834,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool } } +// BuildInboundClientDataMessage builds a message with client data for the given inbound and protocol. func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) { var message string @@ -1864,6 +1884,7 @@ func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol mo return message, nil } +// BuildJSONForProtocol builds a JSON string for the given protocol with client data. func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) { var jsonString string @@ -1942,6 +1963,7 @@ func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) { return jsonString, nil } +// SubmitAddClient submits the client addition request to the inbound service. func (t *Tgbot) SubmitAddClient() (bool, error) { inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) @@ -1964,6 +1986,7 @@ func (t *Tgbot) SubmitAddClient() (bool, error) { return t.inboundService.AddInboundClient(newInbound) } +// checkAdmin checks if the given Telegram ID is an admin. func checkAdmin(tgId int64) bool { for _, adminId := range adminIds { if adminId == tgId { @@ -1973,6 +1996,7 @@ func checkAdmin(tgId int64) bool { return false } +// SendAnswer sends a response message with an inline keyboard to the specified chat. func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { numericKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( @@ -2028,6 +2052,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { t.SendMsgToTgbot(chatId, msg, ReplyMarkup) } +// SendMsgToTgbot sends a message to the Telegram bot with optional reply markup. func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) { if !isRunning { return @@ -2143,6 +2168,7 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) { return subURL, subJsonURL, nil } +// sendClientSubLinks sends the subscription links for the client to the chat. func (t *Tgbot) sendClientSubLinks(chatId int64, email string) { subURL, subJsonURL, err := t.buildSubscriptionURLs(email) if err != nil { @@ -2338,6 +2364,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) { } } +// SendMsgToTgbotAdmins sends a message to all admin Telegram chats. func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) { if len(replyMarkup) > 0 { for _, adminId := range adminIds { @@ -2350,6 +2377,7 @@ func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMark } } +// SendReport sends a periodic report to admin chats. func (t *Tgbot) SendReport() { runTime, err := t.settingService.GetTgbotRuntime() if err == nil && len(runTime) > 0 { @@ -2371,6 +2399,7 @@ func (t *Tgbot) SendReport() { } } +// SendBackupToAdmins sends a database backup to admin chats. func (t *Tgbot) SendBackupToAdmins() { if !t.IsRunning() { return @@ -2380,6 +2409,7 @@ func (t *Tgbot) SendBackupToAdmins() { } } +// sendExhaustedToAdmins sends notifications about exhausted clients to admins. func (t *Tgbot) sendExhaustedToAdmins() { if !t.IsRunning() { return @@ -2389,6 +2419,7 @@ func (t *Tgbot) sendExhaustedToAdmins() { } } +// getServerUsage retrieves and formats server usage information. func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string { info := t.prepareServerUsageInfo() @@ -2410,6 +2441,7 @@ func (t *Tgbot) sendServerUsage() string { return info } +// prepareServerUsageInfo prepares the server usage information string. func (t *Tgbot) prepareServerUsageInfo() string { info, ipv4, ipv6 := "", "", "" @@ -2459,6 +2491,7 @@ func (t *Tgbot) prepareServerUsageInfo() string { return info } +// UserLoginNotify sends a notification about user login attempts to admins. func (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) { if !t.IsRunning() { return @@ -2490,6 +2523,7 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim t.SendMsgToTgbotAdmins(msg) } +// getInboundUsages retrieves and formats inbound usage information. func (t *Tgbot) getInboundUsages() string { info := "" // get traffic @@ -2515,6 +2549,8 @@ func (t *Tgbot) getInboundUsages() string { } return info } + +// getInbounds creates an inline keyboard with all inbounds. func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) { inbounds, err := t.inboundService.GetAllInbounds() if err != nil { @@ -2546,8 +2582,7 @@ func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) { return keyboard, nil } -// getInboundsFor builds an inline keyboard of inbounds where each button leads to a custom next action -// nextAction should be one of: get_clients_for_sub|get_clients_for_individual|get_clients_for_qr +// getInboundsFor builds an inline keyboard of inbounds for a custom next action. func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) { inbounds, err := t.inboundService.GetAllInbounds() if err != nil { @@ -2614,6 +2649,7 @@ func (t *Tgbot) getInboundClientsFor(inboundID int, action string) (*telego.Inli return keyboard, nil } +// getInboundsAddClient creates an inline keyboard for adding clients to inbounds. func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) { inbounds, err := t.inboundService.GetAllInbounds() if err != nil { @@ -2656,6 +2692,7 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) { return keyboard, nil } +// getInboundClients creates an inline keyboard with clients of a specific inbound. func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) { inbound, err := t.inboundService.GetInbound(id) if err != nil { @@ -2690,6 +2727,7 @@ func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) return keyboard, nil } +// clientInfoMsg formats client information message based on traffic and flags. func (t *Tgbot) clientInfoMsg( traffic *xray.ClientTraffic, printEnabled bool, @@ -2796,6 +2834,7 @@ func (t *Tgbot) clientInfoMsg( return output } +// getClientUsage retrieves and sends client usage information to the chat. func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) { traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID) if err != nil { @@ -2838,6 +2877,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) { t.SendAnswer(chatId, output, false) } +// searchClientIps searches and sends client IP addresses for the given email. func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) { ips, err := t.inboundService.GetInboundClientIps(email) if err != nil || len(ips) == 0 { @@ -2865,6 +2905,7 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) { } } +// clientTelegramUserInfo retrieves and sends Telegram user info for the client. func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) { traffic, client, err := t.inboundService.GetClientByEmail(email) if err != nil { @@ -2917,6 +2958,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ... } } +// searchClient searches for a client by email and sends the information. func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { traffic, err := t.inboundService.GetClientTrafficByEmail(email) if err != nil { @@ -2962,6 +3004,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { } } +// addClient handles the process of adding a new client to an inbound. func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { @@ -3058,6 +3101,7 @@ func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { } +// searchInbound searches for inbounds by remark and sends the results. func (t *Tgbot) searchInbound(chatId int64, remark string) { inbounds, err := t.inboundService.SearchInbounds(remark) if err != nil { @@ -3095,6 +3139,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) { } } +// getExhausted retrieves and sends information about exhausted clients. func (t *Tgbot) getExhausted(chatId int64) { trDiff := int64(0) exDiff := int64(0) @@ -3191,6 +3236,7 @@ func (t *Tgbot) getExhausted(chatId int64) { } } +// notifyExhausted sends notifications for exhausted clients. func (t *Tgbot) notifyExhausted() { trDiff := int64(0) exDiff := int64(0) @@ -3262,6 +3308,7 @@ func (t *Tgbot) notifyExhausted() { } } +// int64Contains checks if an int64 slice contains a specific item. func int64Contains(slice []int64, item int64) bool { for _, s := range slice { if s == item { @@ -3271,6 +3318,7 @@ func int64Contains(slice []int64, item int64) bool { return false } +// onlineClients retrieves and sends information about online clients. func (t *Tgbot) onlineClients(chatId int64, messageID ...int) { if !p.IsRunning() { return @@ -3305,6 +3353,7 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) { } } +// sendBackup sends a backup of the database and configuration files. func (t *Tgbot) sendBackup(chatId int64) { output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbot(chatId, output) @@ -3344,6 +3393,7 @@ func (t *Tgbot) sendBackup(chatId int64) { } } +// sendBanLogs sends the ban logs to the specified chat. func (t *Tgbot) sendBanLogs(chatId int64, dt bool) { if dt { output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05")) @@ -3393,6 +3443,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) { } } +// sendCallbackAnswerTgBot answers a callback query with a message. func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) { params := telego.AnswerCallbackQueryParams{ CallbackQueryID: id, @@ -3403,6 +3454,7 @@ func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) { } } +// editMessageCallbackTgBot edits the reply markup of a message. func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) { params := telego.EditMessageReplyMarkupParams{ ChatID: tu.ID(chatId), @@ -3414,6 +3466,7 @@ func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyb } } +// editMessageTgBot edits the text and reply markup of a message. func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) { params := telego.EditMessageTextParams{ ChatID: tu.ID(chatId), @@ -3429,6 +3482,7 @@ func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlin } } +// SendMsgToTgbotDeleteAfter sends a message and deletes it after a specified delay. func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) { // Determine if replyMarkup was passed; otherwise, set it to nil var replyMarkupParam telego.ReplyMarkup @@ -3455,6 +3509,7 @@ func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSecon }() } +// deleteMessageTgBot deletes a message from the chat. func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) { params := telego.DeleteMessageParams{ ChatID: tu.ID(chatId), @@ -3467,6 +3522,7 @@ func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) { } } +// isSingleWord checks if the text contains only a single word. func (t *Tgbot) isSingleWord(text string) bool { text = strings.TrimSpace(text) re := regexp.MustCompile(`\s+`) diff --git a/web/service/user.go b/web/service/user.go index 074eb7ef..f42c3cf8 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -12,10 +12,14 @@ import ( "gorm.io/gorm" ) +// UserService provides business logic for user management and authentication. +// It handles user creation, login, password management, and 2FA operations. type UserService struct { settingService SettingService } +// GetFirstUser retrieves the first user from the database. +// This is typically used for initial setup or when there's only one admin user. func (s *UserService) GetFirstUser() (*model.User, error) { db := database.GetDB() diff --git a/web/service/warp.go b/web/service/warp.go index ef9e573b..6b048ea6 100644 --- a/web/service/warp.go +++ b/web/service/warp.go @@ -12,6 +12,8 @@ import ( "github.com/mhsanaei/3x-ui/v2/util/common" ) +// WarpService provides business logic for Cloudflare WARP integration. +// It manages WARP configuration and connectivity settings. type WarpService struct { SettingService } diff --git a/web/service/xray.go b/web/service/xray.go index 93cbd9f0..43178d2f 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -20,16 +20,20 @@ var ( result string ) +// XrayService provides business logic for Xray process management. +// It handles starting, stopping, restarting Xray, and managing its configuration. type XrayService struct { inboundService InboundService settingService SettingService xrayAPI xray.XrayAPI } +// IsXrayRunning checks if the Xray process is currently running. func (s *XrayService) IsXrayRunning() bool { return p != nil && p.IsRunning() } +// GetXrayErr returns the error from the Xray process, if any. func (s *XrayService) GetXrayErr() error { if p == nil { return nil @@ -46,6 +50,7 @@ func (s *XrayService) GetXrayErr() error { return err } +// GetXrayResult returns the result string from the Xray process. func (s *XrayService) GetXrayResult() string { if result != "" { return result @@ -68,6 +73,7 @@ func (s *XrayService) GetXrayResult() string { return result } +// GetXrayVersion returns the version of the running Xray process. func (s *XrayService) GetXrayVersion() string { if p == nil { return "Unknown" @@ -75,10 +81,13 @@ func (s *XrayService) GetXrayVersion() string { return p.GetVersion() } +// RemoveIndex removes an element at the specified index from a slice. +// Returns a new slice with the element removed. func RemoveIndex(s []any, index int) []any { return append(s[:index], s[index+1:]...) } +// GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds. func (s *XrayService) GetXrayConfig() (*xray.Config, error) { templateConfig, err := s.settingService.GetXrayConfigTemplate() if err != nil { @@ -182,6 +191,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { return xrayConfig, nil } +// GetXrayTraffic fetches the current traffic statistics from the running Xray process. func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) { if !s.IsXrayRunning() { err := errors.New("xray is not running") @@ -200,6 +210,7 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, return traffic, clientTraffic, nil } +// RestartXray restarts the Xray process, optionally forcing a restart even if config unchanged. func (s *XrayService) RestartXray(isForce bool) error { lock.Lock() defer lock.Unlock() @@ -229,6 +240,7 @@ func (s *XrayService) RestartXray(isForce bool) error { return nil } +// StopXray stops the running Xray process. func (s *XrayService) StopXray() error { lock.Lock() defer lock.Unlock() @@ -240,15 +252,17 @@ func (s *XrayService) StopXray() error { return errors.New("xray is not running") } +// SetToNeedRestart marks that Xray needs to be restarted. func (s *XrayService) SetToNeedRestart() { isNeedXrayRestart.Store(true) } +// IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false. func (s *XrayService) IsNeedRestartAndSetFalse() bool { return isNeedXrayRestart.CompareAndSwap(true, false) } -// Check if Xray is not running and wasn't stopped manually, i.e. crashed +// DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped. func (s *XrayService) DidXrayCrash() bool { return !s.IsXrayRunning() && !isManuallyStopped.Load() } diff --git a/web/service/xray_setting.go b/web/service/xray_setting.go index 06131ea3..5df8a211 100644 --- a/web/service/xray_setting.go +++ b/web/service/xray_setting.go @@ -8,6 +8,8 @@ import ( "github.com/mhsanaei/3x-ui/v2/xray" ) +// XraySettingService provides business logic for Xray configuration management. +// It handles validation and storage of Xray template configurations. type XraySettingService struct { SettingService } diff --git a/web/session/session.go b/web/session/session.go index b201f15d..63da426b 100644 --- a/web/session/session.go +++ b/web/session/session.go @@ -1,3 +1,5 @@ +// Package session provides session management utilities for the 3x-ui web panel. +// It handles user authentication state, login sessions, and session storage using Gin sessions. package session import ( @@ -19,6 +21,8 @@ func init() { gob.Register(model.User{}) } +// SetLoginUser stores the authenticated user in the session. +// The user object is serialized and stored for subsequent requests. func SetLoginUser(c *gin.Context, user *model.User) { if user == nil { return @@ -27,6 +31,8 @@ func SetLoginUser(c *gin.Context, user *model.User) { s.Set(loginUserKey, *user) } +// SetMaxAge configures the session cookie maximum age in seconds. +// This controls how long the session remains valid before requiring re-authentication. func SetMaxAge(c *gin.Context, maxAge int) { s := sessions.Default(c) s.Options(sessions.Options{ @@ -37,6 +43,8 @@ func SetMaxAge(c *gin.Context, maxAge int) { }) } +// GetLoginUser retrieves the authenticated user from the session. +// Returns nil if no user is logged in or if the session data is invalid. func GetLoginUser(c *gin.Context) *model.User { s := sessions.Default(c) obj := s.Get(loginUserKey) @@ -52,10 +60,14 @@ func GetLoginUser(c *gin.Context) *model.User { return &user } +// IsLogin checks if a user is currently authenticated in the session. +// Returns true if a valid user session exists, false otherwise. func IsLogin(c *gin.Context) bool { return GetLoginUser(c) != nil } +// ClearSession removes all session data and invalidates the session. +// This effectively logs out the user and clears any stored session information. func ClearSession(c *gin.Context) { s := sessions.Default(c) s.Clear() diff --git a/web/web.go b/web/web.go index daf312cb..c256112c 100644 --- a/web/web.go +++ b/web/web.go @@ -1,3 +1,5 @@ +// Package web provides the main web server implementation for the 3x-ui panel, +// including HTTP/HTTPS serving, routing, templates, and background job scheduling. package web import ( @@ -78,15 +80,17 @@ func (f *wrapAssetsFileInfo) ModTime() time.Time { return startTime } -// Expose embedded resources for reuse by other servers (e.g., sub server) +// EmbeddedHTML returns the embedded HTML templates filesystem for reuse by other servers. func EmbeddedHTML() embed.FS { return htmlFS } +// EmbeddedAssets returns the embedded assets filesystem for reuse by other servers. func EmbeddedAssets() embed.FS { return assetsFS } +// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs. type Server struct { httpServer *http.Server listener net.Listener @@ -106,6 +110,7 @@ type Server struct { cancel context.CancelFunc } +// NewServer creates a new web server instance with a cancellable context. func NewServer() *Server { ctx, cancel := context.WithCancel(context.Background()) return &Server{ @@ -114,6 +119,8 @@ func NewServer() *Server { } } +// getHtmlFiles walks the local `web/html` directory and returns a list of +// template file paths. Used only in debug/development mode. func (s *Server) getHtmlFiles() ([]string, error) { files := make([]string, 0) dir, _ := os.Getwd() @@ -133,6 +140,9 @@ func (s *Server) getHtmlFiles() ([]string, error) { return files, nil } +// getHtmlTemplate parses embedded HTML templates from the bundled `htmlFS` +// using the provided template function map and returns the resulting +// template set for production usage. func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) { t := template.New("").Funcs(funcMap) err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error { @@ -156,6 +166,8 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, return t, nil } +// initRouter initializes Gin, registers middleware, templates, static +// assets, controllers and returns the configured engine. func (s *Server) initRouter() (*gin.Engine, error) { if config.IsDebug() { gin.SetMode(gin.DebugMode) @@ -259,6 +271,8 @@ func (s *Server) initRouter() (*gin.Engine, error) { return engine, nil } +// startTask schedules background jobs (Xray checks, traffic jobs, cron +// jobs) which the panel relies on for periodic maintenance and monitoring. func (s *Server) startTask() { err := s.xrayService.RestartXray(true) if err != nil { @@ -326,6 +340,7 @@ func (s *Server) startTask() { } } +// Start initializes and starts the web server with configured settings, routes, and background jobs. func (s *Server) Start() (err error) { // This is an anonymous function, no function name defer func() { @@ -404,6 +419,7 @@ func (s *Server) Start() (err error) { return nil } +// Stop gracefully shuts down the web server, stops Xray, cron jobs, and Telegram bot. func (s *Server) Stop() error { s.cancel() s.xrayService.StopXray() @@ -424,10 +440,12 @@ func (s *Server) Stop() error { return common.Combine(err1, err2) } +// GetCtx returns the server's context for cancellation and deadline management. func (s *Server) GetCtx() context.Context { return s.ctx } +// GetCron returns the server's cron scheduler instance. func (s *Server) GetCron() *cron.Cron { return s.cron } diff --git a/xray/api.go b/xray/api.go index d7ca4114..11119709 100644 --- a/xray/api.go +++ b/xray/api.go @@ -1,3 +1,6 @@ +// Package xray provides integration with the Xray proxy core. +// It includes API client functionality, configuration management, traffic monitoring, +// and process control for Xray instances. package xray import ( @@ -25,6 +28,7 @@ import ( "google.golang.org/grpc/credentials/insecure" ) +// XrayAPI is a gRPC client for managing Xray core configuration, inbounds, outbounds, and statistics. type XrayAPI struct { HandlerServiceClient *command.HandlerServiceClient StatsServiceClient *statsService.StatsServiceClient @@ -32,6 +36,7 @@ type XrayAPI struct { isConnected bool } +// Init connects to the Xray API server and initializes handler and stats service clients. func (x *XrayAPI) Init(apiPort int) error { if apiPort <= 0 || apiPort > math.MaxUint16 { return fmt.Errorf("invalid Xray API port: %d", apiPort) @@ -55,6 +60,7 @@ func (x *XrayAPI) Init(apiPort int) error { return nil } +// Close closes the gRPC connection and resets the XrayAPI client state. func (x *XrayAPI) Close() { if x.grpcClient != nil { x.grpcClient.Close() @@ -64,6 +70,7 @@ func (x *XrayAPI) Close() { x.isConnected = false } +// AddInbound adds a new inbound configuration to the Xray core via gRPC. func (x *XrayAPI) AddInbound(inbound []byte) error { client := *x.HandlerServiceClient @@ -85,6 +92,7 @@ func (x *XrayAPI) AddInbound(inbound []byte) error { return err } +// DelInbound removes an inbound configuration from the Xray core by tag. func (x *XrayAPI) DelInbound(tag string) error { client := *x.HandlerServiceClient _, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{ @@ -93,6 +101,7 @@ func (x *XrayAPI) DelInbound(tag string) error { return err } +// AddUser adds a user to an inbound in the Xray core using the specified protocol and user data. func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error { var account *serial.TypedMessage switch Protocol { @@ -153,6 +162,7 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an return err } +// RemoveUser removes a user from an inbound in the Xray core by email. func (x *XrayAPI) RemoveUser(inboundTag, email string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -171,6 +181,7 @@ func (x *XrayAPI) RemoveUser(inboundTag, email string) error { return nil } +// GetTraffic queries traffic statistics from the Xray core, optionally resetting counters. func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { if x.grpcClient == nil { return nil, nil, common.NewError("xray api is not initialized") @@ -205,6 +216,7 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil } +// processTraffic aggregates a traffic stat into trafficMap using regex matches and value. func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) { isInbound := matches[1] == "inbound" tag := matches[2] @@ -231,6 +243,7 @@ func processTraffic(matches []string, value int64, trafficMap map[string]*Traffi } } +// processClientTraffic updates clientTrafficMap with upload/download values for a client email. func processClientTraffic(matches []string, value int64, clientTrafficMap map[string]*ClientTraffic) { email := matches[1] isDown := matches[2] == "downlink" @@ -248,6 +261,7 @@ func processClientTraffic(matches []string, value int64, clientTrafficMap map[st } } +// mapToSlice converts a map of pointers to a slice of pointers. func mapToSlice[T any](m map[string]*T) []*T { result := make([]*T, 0, len(m)) for _, v := range m { diff --git a/xray/client_traffic.go b/xray/client_traffic.go index 84c509ac..4bb164d2 100644 --- a/xray/client_traffic.go +++ b/xray/client_traffic.go @@ -1,5 +1,7 @@ package xray +// ClientTraffic represents traffic statistics and limits for a specific client. +// It tracks upload/download usage, expiry times, and online status for inbound clients. type ClientTraffic struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` InboundId int `json:"inboundId" form:"inboundId"` diff --git a/xray/config.go b/xray/config.go index b10592d0..2b4a6673 100644 --- a/xray/config.go +++ b/xray/config.go @@ -6,6 +6,8 @@ import ( "github.com/mhsanaei/3x-ui/v2/util/json_util" ) +// Config represents the complete Xray configuration structure. +// It contains all sections of an Xray config file including inbounds, outbounds, routing, etc. type Config struct { LogConfig json_util.RawMessage `json:"log"` RouterConfig json_util.RawMessage `json:"routing"` @@ -23,6 +25,7 @@ type Config struct { Metrics json_util.RawMessage `json:"metrics"` } +// Equals compares two Config instances for deep equality. func (c *Config) Equals(other *Config) bool { if len(c.InboundConfigs) != len(other.InboundConfigs) { return false diff --git a/xray/inbound.go b/xray/inbound.go index 5a328f31..cbe4392d 100644 --- a/xray/inbound.go +++ b/xray/inbound.go @@ -6,6 +6,8 @@ import ( "github.com/mhsanaei/3x-ui/v2/util/json_util" ) +// InboundConfig represents an Xray inbound configuration. +// It defines how Xray accepts incoming connections including protocol, port, and settings. type InboundConfig struct { Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string Port int `json:"port"` @@ -16,6 +18,7 @@ type InboundConfig struct { Sniffing json_util.RawMessage `json:"sniffing"` } +// Equals compares two InboundConfig instances for deep equality. func (c *InboundConfig) Equals(other *InboundConfig) bool { if !bytes.Equal(c.Listen, other.Listen) { return false diff --git a/xray/log_writer.go b/xray/log_writer.go index 3be39b3e..b2b3b04a 100644 --- a/xray/log_writer.go +++ b/xray/log_writer.go @@ -8,14 +8,17 @@ import ( "github.com/mhsanaei/3x-ui/v2/logger" ) +// NewLogWriter returns a new LogWriter for processing Xray log output. func NewLogWriter() *LogWriter { return &LogWriter{} } +// LogWriter processes and filters log output from the Xray process, handling crash detection and message filtering. type LogWriter struct { lastLine string } +// Write processes and filters log output from the Xray process, handling crash detection and message filtering. func (lw *LogWriter) Write(m []byte) (n int, err error) { crashRegex := regexp.MustCompile(`(?i)(panic|exception|stack trace|fatal error)`) diff --git a/xray/process.go b/xray/process.go index 0df9c44b..f45d6cc9 100644 --- a/xray/process.go +++ b/xray/process.go @@ -18,46 +18,57 @@ import ( "github.com/mhsanaei/3x-ui/v2/util/common" ) +// GetBinaryName returns the Xray binary filename for the current OS and architecture. func GetBinaryName() string { return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH) } +// GetBinaryPath returns the full path to the Xray binary executable. func GetBinaryPath() string { return config.GetBinFolderPath() + "/" + GetBinaryName() } +// GetConfigPath returns the path to the Xray configuration file in the binary folder. func GetConfigPath() string { return config.GetBinFolderPath() + "/config.json" } +// GetGeositePath returns the path to the geosite data file used by Xray. func GetGeositePath() string { return config.GetBinFolderPath() + "/geosite.dat" } +// GetGeoipPath returns the path to the geoip data file used by Xray. func GetGeoipPath() string { return config.GetBinFolderPath() + "/geoip.dat" } +// GetIPLimitLogPath returns the path to the IP limit log file. func GetIPLimitLogPath() string { return config.GetLogFolder() + "/3xipl.log" } +// GetIPLimitBannedLogPath returns the path to the banned IP log file. func GetIPLimitBannedLogPath() string { return config.GetLogFolder() + "/3xipl-banned.log" } +// GetIPLimitBannedPrevLogPath returns the path to the previous banned IP log file. func GetIPLimitBannedPrevLogPath() string { return config.GetLogFolder() + "/3xipl-banned.prev.log" } +// GetAccessPersistentLogPath returns the path to the persistent access log file. func GetAccessPersistentLogPath() string { return config.GetLogFolder() + "/3xipl-ap.log" } +// GetAccessPersistentPrevLogPath returns the path to the previous persistent access log file. func GetAccessPersistentPrevLogPath() string { return config.GetLogFolder() + "/3xipl-ap.prev.log" } +// GetAccessLogPath reads the Xray config and returns the access log file path. func GetAccessLogPath() (string, error) { config, err := os.ReadFile(GetConfigPath()) if err != nil { @@ -82,14 +93,17 @@ func GetAccessLogPath() (string, error) { return "", err } +// stopProcess calls Stop on the given Process instance. func stopProcess(p *Process) { p.Stop() } +// Process wraps an Xray process instance and provides management methods. type Process struct { *process } +// NewProcess creates a new Xray process and sets up cleanup on garbage collection. func NewProcess(xrayConfig *Config) *Process { p := &Process{newProcess(xrayConfig)} runtime.SetFinalizer(p, stopProcess) @@ -110,6 +124,7 @@ type process struct { startTime time.Time } +// newProcess creates a new internal process struct for Xray. func newProcess(config *Config) *process { return &process{ version: "Unknown", @@ -119,6 +134,7 @@ func newProcess(config *Config) *process { } } +// IsRunning returns true if the Xray process is currently running. func (p *process) IsRunning() bool { if p.cmd == nil || p.cmd.Process == nil { return false @@ -129,10 +145,12 @@ func (p *process) IsRunning() bool { return false } +// GetErr returns the last error encountered by the Xray process. func (p *process) GetErr() error { return p.exitErr } +// GetResult returns the last log line or error from the Xray process. func (p *process) GetResult() string { if len(p.logWriter.lastLine) == 0 && p.exitErr != nil { return p.exitErr.Error() @@ -140,30 +158,37 @@ func (p *process) GetResult() string { return p.logWriter.lastLine } +// GetVersion returns the version string of the Xray process. func (p *process) GetVersion() string { return p.version } +// GetAPIPort returns the API port used by the Xray process. func (p *Process) GetAPIPort() int { return p.apiPort } +// GetConfig returns the configuration used by the Xray process. func (p *Process) GetConfig() *Config { return p.config } +// GetOnlineClients returns the list of online clients for the Xray process. func (p *Process) GetOnlineClients() []string { return p.onlineClients } +// SetOnlineClients sets the list of online clients for the Xray process. func (p *Process) SetOnlineClients(users []string) { p.onlineClients = users } +// GetUptime returns the uptime of the Xray process in seconds. func (p *Process) GetUptime() uint64 { return uint64(time.Since(p.startTime).Seconds()) } +// refreshAPIPort updates the API port from the inbound configs. func (p *process) refreshAPIPort() { for _, inbound := range p.config.InboundConfigs { if inbound.Tag == "api" { @@ -173,6 +198,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") data, err := cmd.Output() @@ -188,6 +214,7 @@ func (p *process) refreshVersion() { } } +// Start launches the Xray process with the current configuration. func (p *process) Start() (err error) { if p.IsRunning() { return errors.New("xray is already running") @@ -245,6 +272,7 @@ func (p *process) Start() (err error) { return nil } +// Stop terminates the running Xray process. func (p *process) Stop() error { if !p.IsRunning() { return errors.New("xray is not running") @@ -257,6 +285,7 @@ func (p *process) Stop() error { } } +// writeCrashReport writes a crash report to the binary folder with a timestamped filename. func writeCrashReport(m []byte) error { crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log" return os.WriteFile(crashReportPath, m, os.ModePerm) diff --git a/xray/traffic.go b/xray/traffic.go index 7b907bae..3547573c 100644 --- a/xray/traffic.go +++ b/xray/traffic.go @@ -1,5 +1,7 @@ package xray +// Traffic represents network traffic statistics for Xray connections. +// It tracks upload and download bytes for inbound or outbound traffic. type Traffic struct { IsInbound bool IsOutbound bool