docs: add comments for all functions

This commit is contained in:
mhsanaei 2025-09-20 09:35:50 +02:00
parent f60682a6b7
commit 6ced549dea
No known key found for this signature in database
GPG key ID: D875CD086CF668A0
63 changed files with 624 additions and 113 deletions

View file

@ -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 package config
import ( import (
@ -16,8 +18,10 @@ var version string
//go:embed name //go:embed name
var name string var name string
// LogLevel represents the logging level for the application.
type LogLevel string type LogLevel string
// Logging level constants
const ( const (
Debug LogLevel = "debug" Debug LogLevel = "debug"
Info LogLevel = "info" Info LogLevel = "info"
@ -26,14 +30,17 @@ const (
Error LogLevel = "error" Error LogLevel = "error"
) )
// GetVersion returns the version string of the 3x-ui application.
func GetVersion() string { func GetVersion() string {
return strings.TrimSpace(version) return strings.TrimSpace(version)
} }
// GetName returns the name of the 3x-ui application.
func GetName() string { func GetName() string {
return strings.TrimSpace(name) return strings.TrimSpace(name)
} }
// GetLogLevel returns the current logging level based on environment variables or defaults to Info.
func GetLogLevel() LogLevel { func GetLogLevel() LogLevel {
if IsDebug() { if IsDebug() {
return Debug return Debug
@ -45,10 +52,12 @@ func GetLogLevel() LogLevel {
return LogLevel(logLevel) return LogLevel(logLevel)
} }
// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.
func IsDebug() bool { func IsDebug() bool {
return os.Getenv("XUI_DEBUG") == "true" 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 { func GetBinFolderPath() string {
binFolderPath := os.Getenv("XUI_BIN_FOLDER") binFolderPath := os.Getenv("XUI_BIN_FOLDER")
if binFolderPath == "" { if binFolderPath == "" {
@ -74,6 +83,7 @@ func getBaseDir() string {
return exeDir return exeDir
} }
// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.
func GetDBFolderPath() string { func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER") dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath != "" { if dbFolderPath != "" {
@ -85,10 +95,12 @@ func GetDBFolderPath() string {
return "/etc/x-ui" return "/etc/x-ui"
} }
// GetDBPath returns the full path to the database file.
func GetDBPath() string { func GetDBPath() string {
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) 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 { func GetLogFolder() string {
logFolderPath := os.Getenv("XUI_LOG_FOLDER") logFolderPath := os.Getenv("XUI_LOG_FOLDER")
if logFolderPath != "" { if logFolderPath != "" {

View file

@ -1,3 +1,5 @@
// Package database provides database initialization, migration, and management utilities
// for the 3x-ui panel using GORM with SQLite.
package database package database
import ( import (
@ -45,6 +47,7 @@ func initModels() error {
return nil return nil
} }
// initUser creates a default admin user if the users table is empty.
func initUser() error { func initUser() error {
empty, err := isTableEmpty("users") empty, err := isTableEmpty("users")
if err != nil { if err != nil {
@ -68,6 +71,7 @@ func initUser() error {
return nil return nil
} }
// runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.
func runSeeders(isUsersEmpty bool) error { func runSeeders(isUsersEmpty bool) error {
empty, err := isTableEmpty("history_of_seeders") empty, err := isTableEmpty("history_of_seeders")
if err != nil { if err != nil {
@ -107,12 +111,14 @@ func runSeeders(isUsersEmpty bool) error {
return nil return nil
} }
// isTableEmpty returns true if the named table contains zero rows.
func isTableEmpty(tableName string) (bool, error) { func isTableEmpty(tableName string) (bool, error) {
var count int64 var count int64
err := db.Table(tableName).Count(&count).Error err := db.Table(tableName).Count(&count).Error
return count == 0, err return count == 0, err
} }
// InitDB sets up the database connection, migrates models, and runs seeders.
func InitDB(dbPath string) error { func InitDB(dbPath string) error {
dir := path.Dir(dbPath) dir := path.Dir(dbPath)
err := os.MkdirAll(dir, fs.ModePerm) err := os.MkdirAll(dir, fs.ModePerm)
@ -151,6 +157,7 @@ func InitDB(dbPath string) error {
return runSeeders(isUsersEmpty) return runSeeders(isUsersEmpty)
} }
// CloseDB closes the database connection if it exists.
func CloseDB() error { func CloseDB() error {
if db != nil { if db != nil {
sqlDB, err := db.DB() sqlDB, err := db.DB()
@ -162,14 +169,17 @@ func CloseDB() error {
return nil return nil
} }
// GetDB returns the global GORM database instance.
func GetDB() *gorm.DB { func GetDB() *gorm.DB {
return db return db
} }
// IsNotFound checks if the given error is a GORM record not found error.
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound 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) { func IsSQLiteDB(file io.ReaderAt) (bool, error) {
signature := []byte("SQLite format 3\x00") signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature)) buf := make([]byte, len(signature))
@ -180,6 +190,7 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) {
return bytes.Equal(buf, signature), nil return bytes.Equal(buf, signature), nil
} }
// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
func Checkpoint() error { func Checkpoint() error {
// Update WAL // Update WAL
err := db.Exec("PRAGMA wal_checkpoint;").Error err := db.Exec("PRAGMA wal_checkpoint;").Error

View file

@ -1,3 +1,4 @@
// Package model defines the database models and data structures used by the 3x-ui panel.
package model package model
import ( import (
@ -7,8 +8,10 @@ import (
"github.com/mhsanaei/3x-ui/v2/xray" "github.com/mhsanaei/3x-ui/v2/xray"
) )
// Protocol represents the protocol type for Xray inbounds.
type Protocol string type Protocol string
// Protocol constants for different Xray inbound protocols
const ( const (
VMESS Protocol = "vmess" VMESS Protocol = "vmess"
VLESS Protocol = "vless" VLESS Protocol = "vless"
@ -20,27 +23,29 @@ const (
WireGuard Protocol = "wireguard" WireGuard Protocol = "wireguard"
) )
// User represents a user account in the 3x-ui panel.
type User struct { type User struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
type Inbound struct { type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
UserId int `json:"-"` UserId int `json:"-"` // Associated user ID
Up int64 `json:"up" form:"up"` Up int64 `json:"up" form:"up"` // Upload traffic in bytes
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"` // Download traffic in bytes
Total int64 `json:"total" form:"total"` Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage
Remark string `json:"remark" form:"remark"` Remark string `json:"remark" form:"remark"` // Human-readable remark
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` 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"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` 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"` 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"` 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"` Listen string `json:"listen" form:"listen"`
Port int `json:"port" form:"port"` Port int `json:"port" form:"port"`
Protocol Protocol `json:"protocol" form:"protocol"` Protocol Protocol `json:"protocol" form:"protocol"`
@ -50,6 +55,7 @@ type Inbound struct {
Sniffing string `json:"sniffing" form:"sniffing"` Sniffing string `json:"sniffing" form:"sniffing"`
} }
// OutboundTraffics tracks traffic statistics for Xray outbound connections.
type OutboundTraffics struct { type OutboundTraffics struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Tag string `json:"tag" form:"tag" gorm:"unique"` Tag string `json:"tag" form:"tag" gorm:"unique"`
@ -58,17 +64,20 @@ type OutboundTraffics struct {
Total int64 `json:"total" form:"total" gorm:"default:0"` Total int64 `json:"total" form:"total" gorm:"default:0"`
} }
// InboundClientIps stores IP addresses associated with inbound clients for access control.
type InboundClientIps struct { type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
Ips string `json:"ips" form:"ips"` Ips string `json:"ips" form:"ips"`
} }
// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
type HistoryOfSeeders struct { type HistoryOfSeeders struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
SeederName string `json:"seederName"` SeederName string `json:"seederName"`
} }
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen listen := i.Listen
if 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 { type Setting struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Key string `json:"key" form:"key"` Key string `json:"key" form:"key"`
Value string `json:"value" form:"value"` Value string `json:"value" form:"value"`
} }
// Client represents a client configuration for Xray inbounds with traffic limits and settings.
type Client struct { type Client struct {
ID string `json:"id"` ID string `json:"id"` // Unique client identifier
Security string `json:"security"` Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
Password string `json:"password"` Password string `json:"password"` // Client password
Flow string `json:"flow"` Flow string `json:"flow"` // Flow control (XTLS)
Email string `json:"email"` Email string `json:"email"` // Client email identifier
LimitIP int `json:"limitIp"` LimitIP int `json:"limitIp"` // IP limit for this client
TotalGB int64 `json:"totalGB" form:"totalGB"` TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
TgID int64 `json:"tgId" form:"tgId"` TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
SubID string `json:"subId" form:"subId"` SubID string `json:"subId" form:"subId"` // Subscription identifier
Comment string `json:"comment" form:"comment"` Comment string `json:"comment" form:"comment"` // Client comment
Reset int `json:"reset" form:"reset"` Reset int `json:"reset" form:"reset"` // Reset period in days
CreatedAt int64 `json:"created_at,omitempty"` CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
UpdatedAt int64 `json:"updated_at,omitempty"` UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
} }
// VLESSSettings contains VLESS protocol-specific configuration settings.
type VLESSSettings struct { type VLESSSettings struct {
Clients []Client `json:"clients"` Clients []Client `json:"clients"` // List of VLESS clients
Decryption string `json:"decryption"` Decryption string `json:"decryption"` // Decryption method
Encryption string `json:"encryption"` Encryption string `json:"encryption"` // Encryption method (usually "none" for VLESS)
Fallbacks []any `json:"fallbacks"` Fallbacks []any `json:"fallbacks"` // Fallback configurations
} }

View file

@ -1,3 +1,5 @@
// Package logger provides logging functionality for the 3x-ui panel with
// buffered log storage and multiple log levels.
package logger package logger
import ( import (
@ -9,7 +11,11 @@ import (
) )
var ( 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 { logBuffer []struct {
time string time string
level logging.Level level logging.Level
@ -21,6 +27,7 @@ func init() {
InitLogger(logging.INFO) InitLogger(logging.INFO)
} }
// InitLogger initializes the logger with the specified logging level.
func InitLogger(level logging.Level) { func InitLogger(level logging.Level) {
newLogger := logging.MustGetLogger("x-ui") newLogger := logging.MustGetLogger("x-ui")
var err error var err error
@ -47,51 +54,61 @@ func InitLogger(level logging.Level) {
logger = newLogger logger = newLogger
} }
// Debug logs a debug message and adds it to the log buffer.
func Debug(args ...any) { func Debug(args ...any) {
logger.Debug(args...) logger.Debug(args...)
addToBuffer("DEBUG", fmt.Sprint(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) { func Debugf(format string, args ...any) {
logger.Debugf(format, args...) logger.Debugf(format, args...)
addToBuffer("DEBUG", fmt.Sprintf(format, args...)) addToBuffer("DEBUG", fmt.Sprintf(format, args...))
} }
// Info logs an info message and adds it to the log buffer.
func Info(args ...any) { func Info(args ...any) {
logger.Info(args...) logger.Info(args...)
addToBuffer("INFO", fmt.Sprint(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) { func Infof(format string, args ...any) {
logger.Infof(format, args...) logger.Infof(format, args...)
addToBuffer("INFO", fmt.Sprintf(format, args...)) addToBuffer("INFO", fmt.Sprintf(format, args...))
} }
// Notice logs a notice message and adds it to the log buffer.
func Notice(args ...any) { func Notice(args ...any) {
logger.Notice(args...) logger.Notice(args...)
addToBuffer("NOTICE", fmt.Sprint(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) { func Noticef(format string, args ...any) {
logger.Noticef(format, args...) logger.Noticef(format, args...)
addToBuffer("NOTICE", fmt.Sprintf(format, args...)) addToBuffer("NOTICE", fmt.Sprintf(format, args...))
} }
// Warning logs a warning message and adds it to the log buffer.
func Warning(args ...any) { func Warning(args ...any) {
logger.Warning(args...) logger.Warning(args...)
addToBuffer("WARNING", fmt.Sprint(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) { func Warningf(format string, args ...any) {
logger.Warningf(format, args...) logger.Warningf(format, args...)
addToBuffer("WARNING", fmt.Sprintf(format, args...)) addToBuffer("WARNING", fmt.Sprintf(format, args...))
} }
// Error logs an error message and adds it to the log buffer.
func Error(args ...any) { func Error(args ...any) {
logger.Error(args...) logger.Error(args...)
addToBuffer("ERROR", fmt.Sprint(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) { func Errorf(format string, args ...any) {
logger.Errorf(format, args...) logger.Errorf(format, args...)
addToBuffer("ERROR", fmt.Sprintf(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 { func GetLogs(c int, level string) []string {
var output []string var output []string
logLevel, _ := logging.LogLevel(level) logLevel, _ := logging.LogLevel(level)

14
main.go
View file

@ -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 package main
import ( import (
@ -22,6 +24,7 @@ import (
"github.com/op/go-logging" "github.com/op/go-logging"
) )
// runWebServer initializes and starts the web server for the 3x-ui panel.
func runWebServer() { func runWebServer() {
log.Printf("Starting %v %v", config.GetName(), config.GetVersion()) 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() { func resetSetting() {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { if err != nil {
@ -127,6 +131,7 @@ func resetSetting() {
} }
} }
// showSetting displays the current panel settings if show is true.
func showSetting(show bool) { func showSetting(show bool) {
if show { if show {
settingService := service.SettingService{} 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) { func updateTgbotEnableSts(status bool) {
settingService := service.SettingService{} settingService := service.SettingService{}
currentTgSts, err := settingService.GetTgbotEnabled() 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) { func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { 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) { func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { 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) { func updateCert(publicKey string, privateKey string) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { 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) { func GetCertificate(getCert bool) {
if getCert { if getCert {
settingService := service.SettingService{} 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) { func GetListenIP(getListen bool) {
if getListen { if getListen {
@ -348,6 +359,7 @@ func GetListenIP(getListen bool) {
} }
} }
// migrateDb performs database migration operations for the 3x-ui panel.
func migrateDb() { func migrateDb() {
inboundService := service.InboundService{} inboundService := service.InboundService{}
@ -360,6 +372,8 @@ func migrateDb() {
fmt.Println("Migration done!") 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() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
runWebServer() runWebServer()

View file

@ -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 package sub
import ( import (
@ -39,6 +41,7 @@ func setEmbeddedTemplates(engine *gin.Engine) error {
return nil return nil
} }
// Server represents the subscription server that serves subscription links and JSON configurations.
type Server struct { type Server struct {
httpServer *http.Server httpServer *http.Server
listener net.Listener listener net.Listener
@ -50,6 +53,7 @@ type Server struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
// NewServer creates a new subscription server instance with a cancellable context.
func NewServer() *Server { func NewServer() *Server {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &Server{ 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) { func (s *Server) initRouter() (*gin.Engine, error) {
// Always run in release mode for the subscription server // Always run in release mode for the subscription server
gin.DefaultWriter = io.Discard gin.DefaultWriter = io.Discard
@ -222,6 +228,7 @@ func (s *Server) getHtmlFiles() ([]string, error) {
return files, nil return files, nil
} }
// Start initializes and starts the subscription server with configured settings.
func (s *Server) Start() (err error) { func (s *Server) Start() (err error) {
// This is an anonymous function, no function name // This is an anonymous function, no function name
defer func() { defer func() {
@ -295,6 +302,7 @@ func (s *Server) Start() (err error) {
return nil return nil
} }
// Stop gracefully shuts down the subscription server and closes the listener.
func (s *Server) Stop() error { func (s *Server) Stop() error {
s.cancel() s.cancel()
@ -309,6 +317,7 @@ func (s *Server) Stop() error {
return common.Combine(err1, err2) return common.Combine(err1, err2)
} }
// GetCtx returns the server's context for cancellation and deadline management.
func (s *Server) GetCtx() context.Context { func (s *Server) GetCtx() context.Context {
return s.ctx return s.ctx
} }

View file

@ -10,6 +10,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// SUBController handles HTTP requests for subscription links and JSON configurations.
type SUBController struct { type SUBController struct {
subTitle string subTitle string
subPath string subPath string
@ -22,6 +23,7 @@ type SUBController struct {
subJsonService *SubJsonService subJsonService *SubJsonService
} }
// NewSUBController creates a new subscription controller with the given configuration.
func NewSUBController( func NewSUBController(
g *gin.RouterGroup, g *gin.RouterGroup,
subPath string, subPath string,
@ -53,6 +55,8 @@ func NewSUBController(
return a return a
} }
// initRouter registers HTTP routes for subscription links and JSON endpoints
// on the provided router group.
func (a *SUBController) initRouter(g *gin.RouterGroup) { func (a *SUBController) initRouter(g *gin.RouterGroup) {
gLink := g.Group(a.subPath) gLink := g.Group(a.subPath)
gLink.GET(":subid", a.subs) 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) { func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c) 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) { func (a *SUBController) subJsons(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
_, host, _, _ := a.subService.ResolveRequest(c) _, 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) { func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", updateInterval) c.Writer.Header().Set("Profile-Update-Interval", updateInterval)

View file

@ -17,6 +17,7 @@ import (
//go:embed default.json //go:embed default.json
var defaultJson string var defaultJson string
// SubJsonService handles JSON subscription configuration generation and management.
type SubJsonService struct { type SubJsonService struct {
configJson map[string]any configJson map[string]any
defaultOutbounds []json_util.RawMessage defaultOutbounds []json_util.RawMessage
@ -28,6 +29,7 @@ type SubJsonService struct {
SubService *SubService 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 { func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
var configJson map[string]any var configJson map[string]any
var defaultOutbounds []json_util.RawMessage 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) { func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
inbounds, err := s.SubService.getInboundsBySubId(subId) inbounds, err := s.SubService.getInboundsBySubId(subId)
if err != nil || len(inbounds) == 0 { if err != nil || len(inbounds) == 0 {

View file

@ -20,6 +20,7 @@ import (
"github.com/mhsanaei/3x-ui/v2/xray" "github.com/mhsanaei/3x-ui/v2/xray"
) )
// SubService provides business logic for generating subscription links and managing subscription data.
type SubService struct { type SubService struct {
address string address string
showInfo bool showInfo bool
@ -29,6 +30,7 @@ type SubService struct {
settingService service.SettingService settingService service.SettingService
} }
// NewSubService creates a new subscription service with the given configuration.
func NewSubService(showInfo bool, remarkModel string) *SubService { func NewSubService(showInfo bool, remarkModel string) *SubService {
return &SubService{ return &SubService{
showInfo: showInfo, 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) { func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {
s.address = host s.address = host
var result []string var result []string
@ -1008,6 +1011,7 @@ func searchHost(headers any) string {
} }
// PageData is a view model for subpage.html // PageData is a view model for subpage.html
// PageData contains data for rendering the subscription information page.
type PageData struct { type PageData struct {
Host string Host string
BasePath string BasePath string
@ -1029,6 +1033,7 @@ type PageData struct {
} }
// ResolveRequest extracts scheme and host info from request/headers consistently. // 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) { func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
// scheme // scheme
scheme = "http" 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 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) { func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
if strings.HasSuffix(subPath, "/") { if strings.HasSuffix(subPath, "/") {
subURL = scheme + "://" + hostWithPort + subPath + subId 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 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 { func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
download := common.FormatTraffic(traffic.Down) download := common.FormatTraffic(traffic.Down)
upload := common.FormatTraffic(traffic.Up) upload := common.FormatTraffic(traffic.Up)

View file

@ -1,3 +1,4 @@
// Package common provides common utility functions for error handling, formatting, and multi-error management.
package common package common
import ( import (
@ -7,16 +8,19 @@ import (
"github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/logger"
) )
// NewErrorf creates a new error with formatted message.
func NewErrorf(format string, a ...any) error { func NewErrorf(format string, a ...any) error {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
return errors.New(msg) return errors.New(msg)
} }
// NewError creates a new error from the given arguments.
func NewError(a ...any) error { func NewError(a ...any) error {
msg := fmt.Sprintln(a...) msg := fmt.Sprintln(a...)
return errors.New(msg) return errors.New(msg)
} }
// Recover handles panic recovery and logs the panic error if a message is provided.
func Recover(msg string) any { func Recover(msg string) any {
panicErr := recover() panicErr := recover()
if panicErr != nil { if panicErr != nil {

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
) )
// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).
func FormatTraffic(trafficBytes int64) string { func FormatTraffic(trafficBytes int64) string {
units := []string{"B", "KB", "MB", "GB", "TB", "PB"} units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
unitIndex := 0 unitIndex := 0

View file

@ -4,8 +4,10 @@ import (
"strings" "strings"
) )
// multiError represents a collection of errors.
type multiError []error type multiError []error
// Error returns a string representation of all errors joined with " | ".
func (e multiError) Error() string { func (e multiError) Error() string {
var r strings.Builder var r strings.Builder
r.WriteString("multierr: ") r.WriteString("multierr: ")
@ -16,6 +18,7 @@ func (e multiError) Error() string {
return r.String() return r.String()
} }
// Combine combines multiple errors into a single error, filtering out nil errors.
func Combine(maybeError ...error) error { func Combine(maybeError ...error) error {
var errs multiError var errs multiError
for _, err := range maybeError { for _, err := range maybeError {

View file

@ -1,14 +1,17 @@
// Package crypto provides cryptographic utilities for password hashing and verification.
package crypto package crypto
import ( import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// HashPasswordAsBcrypt generates a bcrypt hash of the given password.
func HashPasswordAsBcrypt(password string) (string, error) { func HashPasswordAsBcrypt(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hash), err return string(hash), err
} }
// CheckPasswordHash verifies if the given password matches the bcrypt hash.
func CheckPasswordHash(hash, password string) bool { func CheckPasswordHash(hash, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil return err == nil

View file

@ -1,12 +1,15 @@
// Package json_util provides JSON utilities including a custom RawMessage type.
package json_util package json_util
import ( import (
"errors" "errors"
) )
// RawMessage is a custom JSON raw message type that marshals empty slices as "null".
type RawMessage []byte 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) { func (m RawMessage) MarshalJSON() ([]byte, error) {
if len(m) == 0 { if len(m) == 0 {
return []byte("null"), nil return []byte("null"), nil
@ -14,7 +17,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
return m, nil 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 { func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil { if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")

View file

@ -1,3 +1,4 @@
// Package random provides utilities for generating random strings and numbers.
package random package random
import ( import (
@ -13,6 +14,8 @@ var (
allSeq [62]rune 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() { func init() {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
numSeq[i] = rune('0' + i) numSeq[i] = rune('0' + i)
@ -33,6 +36,7 @@ func init() {
copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:]) 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 { func Seq(n int) string {
runes := make([]rune, n) runes := make([]rune, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
@ -41,6 +45,7 @@ func Seq(n int) string {
return string(runes) return string(runes)
} }
// Num generates a random integer between 0 and n-1.
func Num(n int) int { func Num(n int) int {
return rand.Intn(n) return rand.Intn(n)
} }

View file

@ -1,7 +1,9 @@
// Package reflect_util provides reflection utilities for working with struct fields and values.
package reflect_util package reflect_util
import "reflect" import "reflect"
// GetFields returns all struct fields of the given reflect.Type.
func GetFields(t reflect.Type) []reflect.StructField { func GetFields(t reflect.Type) []reflect.StructField {
num := t.NumField() num := t.NumField()
fields := make([]reflect.StructField, 0, num) fields := make([]reflect.StructField, 0, num)
@ -11,6 +13,7 @@ func GetFields(t reflect.Type) []reflect.StructField {
return fields return fields
} }
// GetFieldValues returns all field values of the given reflect.Value.
func GetFieldValues(v reflect.Value) []reflect.Value { func GetFieldValues(v reflect.Value) []reflect.Value {
num := v.NumField() num := v.NumField()
fields := make([]reflect.Value, 0, num) fields := make([]reflect.Value, 0, num)

View file

@ -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 package sys
import ( import (

View file

@ -45,6 +45,8 @@ func getLinesNum(filename string) (int, error) {
return sum, nil 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) { func GetTCPCount() (int, error) {
root := HostProc() root := HostProc()
@ -75,6 +77,8 @@ func GetUDPCount() (int, error) {
return udp4 + udp6, nil 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) { func safeGetLinesNum(path string) (int, error) {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return 0, nil return 0, nil

View file

@ -12,6 +12,7 @@ import (
"github.com/shirou/gopsutil/v4/net" "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) { func GetConnectionCount(proto string) (int, error) {
if proto != "tcp" && proto != "udp" { if proto != "tcp" && proto != "udp" {
return 0, errors.New("invalid protocol") return 0, errors.New("invalid protocol")
@ -24,10 +25,12 @@ func GetConnectionCount(proto string) (int, error) {
return len(stats), nil return len(stats), nil
} }
// GetTCPCount returns the number of active TCP connections.
func GetTCPCount() (int, error) { func GetTCPCount() (int, error) {
return GetConnectionCount("tcp") return GetConnectionCount("tcp")
} }
// GetUDPCount returns the number of active UDP connections.
func GetUDPCount() (int, error) { func GetUDPCount() (int, error) {
return GetConnectionCount("udp") return GetConnectionCount("udp")
} }
@ -50,6 +53,8 @@ type filetime struct {
HighDateTime uint32 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 { func ftToUint64(ft filetime) uint64 {
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime) return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
} }

View file

@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
type APIController struct { type APIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
@ -13,12 +14,14 @@ type APIController struct {
Tgbot service.Tgbot Tgbot service.Tgbot
} }
// NewAPIController creates a new APIController instance and initializes its routes.
func NewAPIController(g *gin.RouterGroup) *APIController { func NewAPIController(g *gin.RouterGroup) *APIController {
a := &APIController{} a := &APIController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the API routes for inbounds, server, and other endpoints.
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
// Main API group // Main API group
api := g.Group("/panel/api") api := g.Group("/panel/api")
@ -36,6 +39,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
api.GET("/backuptotgbot", a.BackuptoTgbot) api.GET("/backuptotgbot", a.BackuptoTgbot)
} }
// BackuptoTgbot sends a backup of the panel data to Telegram bot admins.
func (a *APIController) BackuptoTgbot(c *gin.Context) { func (a *APIController) BackuptoTgbot(c *gin.Context) {
a.Tgbot.SendBackupToAdmins() a.Tgbot.SendBackupToAdmins()
} }

View file

@ -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 package controller
import ( import (
@ -10,8 +12,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// BaseController provides common functionality for all controllers, including authentication checks.
type BaseController struct{} type BaseController struct{}
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
func (a *BaseController) checkLogin(c *gin.Context) { func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) { if !session.IsLogin(c) {
if isAjax(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 { func I18nWeb(c *gin.Context, name string, params ...string) string {
anyfunc, funcExists := c.Get("I18n") anyfunc, funcExists := c.Get("I18n")
if !funcExists { if !funcExists {

View file

@ -12,17 +12,20 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// InboundController handles HTTP requests related to Xray inbounds management.
type InboundController struct { type InboundController struct {
inboundService service.InboundService inboundService service.InboundService
xrayService service.XrayService xrayService service.XrayService
} }
// NewInboundController creates a new InboundController and sets up its routes.
func NewInboundController(g *gin.RouterGroup) *InboundController { func NewInboundController(g *gin.RouterGroup) *InboundController {
a := &InboundController{} a := &InboundController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter initializes the routes for inbound-related operations.
func (a *InboundController) initRouter(g *gin.RouterGroup) { func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.GET("/list", a.getInbounds) g.GET("/list", a.getInbounds)
@ -49,6 +52,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail) 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) { func (a *InboundController) getInbounds(c *gin.Context) {
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id) inbounds, err := a.inboundService.GetInbounds(user.Id)
@ -59,6 +63,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
jsonObj(c, inbounds, nil) jsonObj(c, inbounds, nil)
} }
// getInbound retrieves a specific inbound by its ID.
func (a *InboundController) getInbound(c *gin.Context) { func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -73,6 +78,7 @@ func (a *InboundController) getInbound(c *gin.Context) {
jsonObj(c, inbound, nil) jsonObj(c, inbound, nil)
} }
// getClientTraffics retrieves client traffic information by email.
func (a *InboundController) getClientTraffics(c *gin.Context) { func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email) clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
@ -83,6 +89,7 @@ func (a *InboundController) getClientTraffics(c *gin.Context) {
jsonObj(c, clientTraffics, nil) jsonObj(c, clientTraffics, nil)
} }
// getClientTrafficsById retrieves client traffic information by inbound ID.
func (a *InboundController) getClientTrafficsById(c *gin.Context) { func (a *InboundController) getClientTrafficsById(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
clientTraffics, err := a.inboundService.GetClientTrafficByID(id) clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
@ -93,6 +100,7 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) {
jsonObj(c, clientTraffics, nil) jsonObj(c, clientTraffics, nil)
} }
// addInbound creates a new inbound configuration.
func (a *InboundController) addInbound(c *gin.Context) { func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := c.ShouldBind(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) { func (a *InboundController) delInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { 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) { func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { 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) { func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
@ -173,6 +184,7 @@ func (a *InboundController) getClientIps(c *gin.Context) {
jsonObj(c, ips, nil) jsonObj(c, ips, nil)
} }
// clearClientIps clears the IP addresses for a client by email.
func (a *InboundController) clearClientIps(c *gin.Context) { func (a *InboundController) clearClientIps(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
@ -184,6 +196,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil) 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) { func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{} data := &model.Inbound{}
err := c.ShouldBind(data) 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) { func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { 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) { func (a *InboundController) updateInboundClient(c *gin.Context) {
clientId := c.Param("clientId") 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) { func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { 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) { func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics() err := a.inboundService.ResetAllTraffics()
if err != nil { if err != nil {
@ -273,6 +290,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil) 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) { func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -290,6 +308,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
} }
// importInbound imports an inbound configuration from provided data.
func (a *InboundController) importInbound(c *gin.Context) { func (a *InboundController) importInbound(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := json.Unmarshal([]byte(c.PostForm("data")), 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) { func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -333,15 +353,18 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil)
} }
// onlines retrieves the list of currently online clients.
func (a *InboundController) onlines(c *gin.Context) { func (a *InboundController) onlines(c *gin.Context) {
jsonObj(c, a.inboundService.GetOnlineClients(), nil) jsonObj(c, a.inboundService.GetOnlineClients(), nil)
} }
// lastOnline retrieves the last online timestamps for clients.
func (a *InboundController) lastOnline(c *gin.Context) { func (a *InboundController) lastOnline(c *gin.Context) {
data, err := a.inboundService.GetClientsLastOnline() data, err := a.inboundService.GetClientsLastOnline()
jsonObj(c, data, err) jsonObj(c, data, err)
} }
// updateClientTraffic updates the traffic statistics for a client by email.
func (a *InboundController) updateClientTraffic(c *gin.Context) { func (a *InboundController) updateClientTraffic(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
@ -367,6 +390,7 @@ func (a *InboundController) updateClientTraffic(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) 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) { func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
inboundId, err := strconv.Atoi(c.Param("id")) inboundId, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {

View file

@ -13,12 +13,14 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// LoginForm represents the login request structure.
type LoginForm struct { type LoginForm struct {
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"` Password string `json:"password" form:"password"`
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"` TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
} }
// IndexController handles the main index and login-related routes.
type IndexController struct { type IndexController struct {
BaseController BaseController
@ -27,12 +29,14 @@ type IndexController struct {
tgbot service.Tgbot tgbot service.Tgbot
} }
// NewIndexController creates a new IndexController and initializes its routes.
func NewIndexController(g *gin.RouterGroup) *IndexController { func NewIndexController(g *gin.RouterGroup) *IndexController {
a := &IndexController{} a := &IndexController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
func (a *IndexController) initRouter(g *gin.RouterGroup) { func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index) g.GET("/", a.index)
g.POST("/login", a.login) g.POST("/login", a.login)
@ -40,6 +44,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable) 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) { func (a *IndexController) index(c *gin.Context) {
if session.IsLogin(c) { if session.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, "panel/") c.Redirect(http.StatusTemporaryRedirect, "panel/")
@ -48,6 +53,7 @@ func (a *IndexController) index(c *gin.Context) {
html(c, "login.html", "pages.login.title", nil) html(c, "login.html", "pages.login.title", nil)
} }
// login handles user authentication and session creation.
func (a *IndexController) login(c *gin.Context) { func (a *IndexController) login(c *gin.Context) {
var form LoginForm var form LoginForm
@ -95,6 +101,7 @@ func (a *IndexController) login(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil) 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) { func (a *IndexController) logout(c *gin.Context) {
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
if user != nil { if user != nil {
@ -107,6 +114,7 @@ func (a *IndexController) logout(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
} }
// getTwoFactorEnable retrieves the current status of two-factor authentication.
func (a *IndexController) getTwoFactorEnable(c *gin.Context) { func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
status, err := a.settingService.GetTwoFactorEnable() status, err := a.settingService.GetTwoFactorEnable()
if err == nil { if err == nil {

View file

@ -15,6 +15,7 @@ import (
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`) var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
// ServerController handles server management and status-related operations.
type ServerController struct { type ServerController struct {
BaseController BaseController
@ -27,6 +28,7 @@ type ServerController struct {
lastGetVersionsTime int64 // unix seconds lastGetVersionsTime int64 // unix seconds
} }
// NewServerController creates a new ServerController, initializes routes, and starts background tasks.
func NewServerController(g *gin.RouterGroup) *ServerController { func NewServerController(g *gin.RouterGroup) *ServerController {
a := &ServerController{} a := &ServerController{}
a.initRouter(g) a.initRouter(g)
@ -34,6 +36,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
return a return a
} }
// initRouter sets up the routes for server status, Xray management, and utility endpoints.
func (a *ServerController) initRouter(g *gin.RouterGroup) { func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.GET("/status", a.status) g.GET("/status", a.status)
@ -58,6 +61,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/getNewEchCert", a.getNewEchCert) g.POST("/getNewEchCert", a.getNewEchCert)
} }
// refreshStatus updates the cached server status and collects CPU history.
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
a.lastStatus = a.serverService.GetStatus(a.lastStatus) a.lastStatus = a.serverService.GetStatus(a.lastStatus)
// collect cpu history when status is fresh // 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() { func (a *ServerController) startTask() {
webServer := global.GetWebServer() webServer := global.GetWebServer()
c := webServer.GetCron() 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) } 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) { func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
bucketStr := c.Param("bucket") bucketStr := c.Param("bucket")
bucket, err := strconv.Atoi(bucketStr) bucket, err := strconv.Atoi(bucketStr)
@ -101,6 +108,7 @@ func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
jsonObj(c, points, nil) jsonObj(c, points, nil)
} }
// getXrayVersion retrieves available Xray versions, with caching for 1 minute.
func (a *ServerController) getXrayVersion(c *gin.Context) { func (a *ServerController) getXrayVersion(c *gin.Context) {
now := time.Now().Unix() now := time.Now().Unix()
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
@ -120,18 +128,21 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
jsonObj(c, versions, nil) jsonObj(c, versions, nil)
} }
// installXray installs or updates Xray to the specified version.
func (a *ServerController) installXray(c *gin.Context) { func (a *ServerController) installXray(c *gin.Context) {
version := c.Param("version") version := c.Param("version")
err := a.serverService.UpdateXray(version) err := a.serverService.UpdateXray(version)
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err) jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
} }
// updateGeofile updates the specified geo file for Xray.
func (a *ServerController) updateGeofile(c *gin.Context) { func (a *ServerController) updateGeofile(c *gin.Context) {
fileName := c.Param("fileName") fileName := c.Param("fileName")
err := a.serverService.UpdateGeofile(fileName) err := a.serverService.UpdateGeofile(fileName)
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err) jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
} }
// stopXrayService stops the Xray service.
func (a *ServerController) stopXrayService(c *gin.Context) { func (a *ServerController) stopXrayService(c *gin.Context) {
err := a.serverService.StopXrayService() err := a.serverService.StopXrayService()
if err != nil { if err != nil {
@ -141,6 +152,7 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err)
} }
// restartXrayService restarts the Xray service.
func (a *ServerController) restartXrayService(c *gin.Context) { func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService() err := a.serverService.RestartXrayService()
if err != nil { if err != nil {
@ -150,6 +162,7 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err) 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) { func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count") count := c.Param("count")
level := c.PostForm("level") level := c.PostForm("level")
@ -158,6 +171,7 @@ func (a *ServerController) getLogs(c *gin.Context) {
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }
// getXrayLogs retrieves Xray logs with filtering options for direct, blocked, and proxy traffic.
func (a *ServerController) getXrayLogs(c *gin.Context) { func (a *ServerController) getXrayLogs(c *gin.Context) {
count := c.Param("count") count := c.Param("count")
filter := c.PostForm("filter") filter := c.PostForm("filter")
@ -202,6 +216,7 @@ func (a *ServerController) getXrayLogs(c *gin.Context) {
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }
// getConfigJson retrieves the Xray configuration as JSON.
func (a *ServerController) getConfigJson(c *gin.Context) { func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson() configJson, err := a.serverService.GetConfigJson()
if err != nil { if err != nil {
@ -211,6 +226,7 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
jsonObj(c, configJson, nil) jsonObj(c, configJson, nil)
} }
// getDb downloads the database file.
func (a *ServerController) getDb(c *gin.Context) { func (a *ServerController) getDb(c *gin.Context) {
db, err := a.serverService.GetDb() db, err := a.serverService.GetDb()
if err != nil { if err != nil {
@ -238,6 +254,7 @@ func isValidFilename(filename string) bool {
return filenameRegex.MatchString(filename) return filenameRegex.MatchString(filename)
} }
// importDB imports a database file and restarts the Xray service.
func (a *ServerController) importDB(c *gin.Context) { func (a *ServerController) importDB(c *gin.Context) {
// Get the file from the request body // Get the file from the request body
file, _, err := c.Request.FormFile("db") 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) jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
} }
// getNewX25519Cert generates a new X25519 certificate.
func (a *ServerController) getNewX25519Cert(c *gin.Context) { func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert() cert, err := a.serverService.GetNewX25519Cert()
if err != nil { if err != nil {
@ -267,6 +285,7 @@ func (a *ServerController) getNewX25519Cert(c *gin.Context) {
jsonObj(c, cert, nil) jsonObj(c, cert, nil)
} }
// getNewmldsa65 generates a new ML-DSA-65 key.
func (a *ServerController) getNewmldsa65(c *gin.Context) { func (a *ServerController) getNewmldsa65(c *gin.Context) {
cert, err := a.serverService.GetNewmldsa65() cert, err := a.serverService.GetNewmldsa65()
if err != nil { if err != nil {
@ -276,6 +295,7 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) {
jsonObj(c, cert, nil) jsonObj(c, cert, nil)
} }
// getNewEchCert generates a new ECH certificate for the given SNI.
func (a *ServerController) getNewEchCert(c *gin.Context) { func (a *ServerController) getNewEchCert(c *gin.Context) {
sni := c.PostForm("sni") sni := c.PostForm("sni")
cert, err := a.serverService.GetNewEchCert(sni) cert, err := a.serverService.GetNewEchCert(sni)
@ -286,6 +306,7 @@ func (a *ServerController) getNewEchCert(c *gin.Context) {
jsonObj(c, cert, nil) jsonObj(c, cert, nil)
} }
// getNewVlessEnc generates a new VLESS encryption key.
func (a *ServerController) getNewVlessEnc(c *gin.Context) { func (a *ServerController) getNewVlessEnc(c *gin.Context) {
out, err := a.serverService.GetNewVlessEnc() out, err := a.serverService.GetNewVlessEnc()
if err != nil { if err != nil {
@ -295,6 +316,7 @@ func (a *ServerController) getNewVlessEnc(c *gin.Context) {
jsonObj(c, out, nil) jsonObj(c, out, nil)
} }
// getNewUUID generates a new UUID.
func (a *ServerController) getNewUUID(c *gin.Context) { func (a *ServerController) getNewUUID(c *gin.Context) {
uuidResp, err := a.serverService.GetNewUUID() uuidResp, err := a.serverService.GetNewUUID()
if err != nil { if err != nil {
@ -305,6 +327,7 @@ func (a *ServerController) getNewUUID(c *gin.Context) {
jsonObj(c, uuidResp, nil) jsonObj(c, uuidResp, nil)
} }
// getNewmlkem768 generates a new ML-KEM-768 key.
func (a *ServerController) getNewmlkem768(c *gin.Context) { func (a *ServerController) getNewmlkem768(c *gin.Context) {
out, err := a.serverService.GetNewmlkem768() out, err := a.serverService.GetNewmlkem768()
if err != nil { if err != nil {

View file

@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// updateUserForm represents the form for updating user credentials.
type updateUserForm struct { type updateUserForm struct {
OldUsername string `json:"oldUsername" form:"oldUsername"` OldUsername string `json:"oldUsername" form:"oldUsername"`
OldPassword string `json:"oldPassword" form:"oldPassword"` OldPassword string `json:"oldPassword" form:"oldPassword"`
@ -19,18 +20,21 @@ type updateUserForm struct {
NewPassword string `json:"newPassword" form:"newPassword"` NewPassword string `json:"newPassword" form:"newPassword"`
} }
// SettingController handles settings and user management operations.
type SettingController struct { type SettingController struct {
settingService service.SettingService settingService service.SettingService
userService service.UserService userService service.UserService
panelService service.PanelService panelService service.PanelService
} }
// NewSettingController creates a new SettingController and initializes its routes.
func NewSettingController(g *gin.RouterGroup) *SettingController { func NewSettingController(g *gin.RouterGroup) *SettingController {
a := &SettingController{} a := &SettingController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the routes for settings management.
func (a *SettingController) initRouter(g *gin.RouterGroup) { func (a *SettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/setting") g = g.Group("/setting")
@ -42,6 +46,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
} }
// getAllSetting retrieves all current settings.
func (a *SettingController) getAllSetting(c *gin.Context) { func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting() allSetting, err := a.settingService.GetAllSetting()
if err != nil { if err != nil {
@ -51,6 +56,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
jsonObj(c, allSetting, nil) jsonObj(c, allSetting, nil)
} }
// getDefaultSettings retrieves the default settings based on the host.
func (a *SettingController) getDefaultSettings(c *gin.Context) { func (a *SettingController) getDefaultSettings(c *gin.Context) {
result, err := a.settingService.GetDefaultSettings(c.Request.Host) result, err := a.settingService.GetDefaultSettings(c.Request.Host)
if err != nil { if err != nil {
@ -60,6 +66,7 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) {
jsonObj(c, result, nil) jsonObj(c, result, nil)
} }
// updateSetting updates all settings with the provided data.
func (a *SettingController) updateSetting(c *gin.Context) { func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{} allSetting := &entity.AllSetting{}
err := c.ShouldBind(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) 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) { func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{} form := &updateUserForm{}
err := c.ShouldBind(form) err := c.ShouldBind(form)
@ -96,11 +104,13 @@ func (a *SettingController) updateUser(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
} }
// restartPanel restarts the panel service after a delay.
func (a *SettingController) restartPanel(c *gin.Context) { func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3) err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err)
} }
// getDefaultXrayConfig retrieves the default Xray configuration.
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) { func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig() defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
if err != nil { if err != nil {

View file

@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// getRemoteIp extracts the real IP address from the request headers or remote address.
func getRemoteIp(c *gin.Context) string { func getRemoteIp(c *gin.Context) string {
value := c.GetHeader("X-Real-IP") value := c.GetHeader("X-Real-IP")
if value != "" { if value != "" {
@ -27,14 +28,17 @@ func getRemoteIp(c *gin.Context) string {
return ip return ip
} }
// jsonMsg sends a JSON response with a message and error status.
func jsonMsg(c *gin.Context, msg string, err error) { func jsonMsg(c *gin.Context, msg string, err error) {
jsonMsgObj(c, msg, nil, err) 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) { func jsonObj(c *gin.Context, obj any, err error) {
jsonMsgObj(c, "", obj, err) 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) { func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
m := entity.Msg{ m := entity.Msg{
Obj: obj, Obj: obj,
@ -52,6 +56,7 @@ func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
c.JSON(http.StatusOK, m) 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) { func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
c.JSON(statusCode, entity.Msg{ c.JSON(statusCode, entity.Msg{
Success: success, 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) { func html(c *gin.Context, name string, title string, data gin.H) {
if data == nil { if data == nil {
data = gin.H{} 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)) 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 { func getContext(h gin.H) gin.H {
a := gin.H{ a := gin.H{
"cur_ver": config.GetVersion(), "cur_ver": config.GetVersion(),
@ -91,6 +98,7 @@ func getContext(h gin.H) gin.H {
return a return a
} }
// isAjax checks if the request is an AJAX request.
func isAjax(c *gin.Context) bool { func isAjax(c *gin.Context) bool {
return c.GetHeader("X-Requested-With") == "XMLHttpRequest" return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
} }

View file

@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// XraySettingController handles Xray configuration and settings operations.
type XraySettingController struct { type XraySettingController struct {
XraySettingService service.XraySettingService XraySettingService service.XraySettingService
SettingService service.SettingService SettingService service.SettingService
@ -15,12 +16,14 @@ type XraySettingController struct {
WarpService service.WarpService WarpService service.WarpService
} }
// NewXraySettingController creates a new XraySettingController and initializes its routes.
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController { func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
a := &XraySettingController{} a := &XraySettingController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the routes for Xray settings management.
func (a *XraySettingController) initRouter(g *gin.RouterGroup) { func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xray") g = g.Group("/xray")
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
@ -33,6 +36,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
} }
// getXraySetting retrieves the Xray configuration template and inbound tags.
func (a *XraySettingController) getXraySetting(c *gin.Context) { func (a *XraySettingController) getXraySetting(c *gin.Context) {
xraySetting, err := a.SettingService.GetXrayConfigTemplate() xraySetting, err := a.SettingService.GetXrayConfigTemplate()
if err != nil { if err != nil {
@ -48,12 +52,14 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
jsonObj(c, xrayResponse, nil) jsonObj(c, xrayResponse, nil)
} }
// updateSetting updates the Xray configuration settings.
func (a *XraySettingController) updateSetting(c *gin.Context) { func (a *XraySettingController) updateSetting(c *gin.Context) {
xraySetting := c.PostForm("xraySetting") xraySetting := c.PostForm("xraySetting")
err := a.XraySettingService.SaveXraySetting(xraySetting) err := a.XraySettingService.SaveXraySetting(xraySetting)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
} }
// getDefaultXrayConfig retrieves the default Xray configuration.
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) { func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig() defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
if err != nil { if err != nil {
@ -63,10 +69,12 @@ func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
jsonObj(c, defaultJsonConfig, nil) jsonObj(c, defaultJsonConfig, nil)
} }
// getXrayResult retrieves the current Xray service result.
func (a *XraySettingController) getXrayResult(c *gin.Context) { func (a *XraySettingController) getXrayResult(c *gin.Context) {
jsonObj(c, a.XrayService.GetXrayResult(), nil) jsonObj(c, a.XrayService.GetXrayResult(), nil)
} }
// warp handles Warp-related operations based on the action parameter.
func (a *XraySettingController) warp(c *gin.Context) { func (a *XraySettingController) warp(c *gin.Context) {
action := c.Param("action") action := c.Param("action")
var resp string var resp string
@ -90,6 +98,7 @@ func (a *XraySettingController) warp(c *gin.Context) {
jsonObj(c, resp, err) jsonObj(c, resp, err)
} }
// getOutboundsTraffic retrieves the traffic statistics for outbounds.
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) { func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic() outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
if err != nil { if err != nil {
@ -99,6 +108,7 @@ func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
jsonObj(c, outboundsTraffic, nil) jsonObj(c, outboundsTraffic, nil)
} }
// resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) { func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
tag := c.PostForm("tag") tag := c.PostForm("tag")
err := a.OutboundService.ResetOutboundTraffic(tag) err := a.OutboundService.ResetOutboundTraffic(tag)

View file

@ -4,6 +4,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// XUIController is the main controller for the X-UI panel, managing sub-controllers.
type XUIController struct { type XUIController struct {
BaseController BaseController
@ -13,12 +14,14 @@ type XUIController struct {
xraySettingController *XraySettingController xraySettingController *XraySettingController
} }
// NewXUIController creates a new XUIController and initializes its routes.
func NewXUIController(g *gin.RouterGroup) *XUIController { func NewXUIController(g *gin.RouterGroup) *XUIController {
a := &XUIController{} a := &XUIController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the main panel routes and initializes sub-controllers.
func (a *XUIController) initRouter(g *gin.RouterGroup) { func (a *XUIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/panel") g = g.Group("/panel")
g.Use(a.checkLogin) g.Use(a.checkLogin)
@ -34,18 +37,22 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
a.xraySettingController = NewXraySettingController(g) a.xraySettingController = NewXraySettingController(g)
} }
// index renders the main panel index page.
func (a *XUIController) index(c *gin.Context) { func (a *XUIController) index(c *gin.Context) {
html(c, "index.html", "pages.index.title", nil) html(c, "index.html", "pages.index.title", nil)
} }
// inbounds renders the inbounds management page.
func (a *XUIController) inbounds(c *gin.Context) { func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "pages.inbounds.title", nil) html(c, "inbounds.html", "pages.inbounds.title", nil)
} }
// settings renders the settings management page.
func (a *XUIController) settings(c *gin.Context) { func (a *XUIController) settings(c *gin.Context) {
html(c, "settings.html", "pages.settings.title", nil) html(c, "settings.html", "pages.settings.title", nil)
} }
// xraySettings renders the Xray settings page.
func (a *XUIController) xraySettings(c *gin.Context) { func (a *XUIController) xraySettings(c *gin.Context) {
html(c, "xray.html", "pages.xray.title", nil) html(c, "xray.html", "pages.xray.title", nil)
} }

View file

@ -1,3 +1,4 @@
// Package entity defines data structures and entities used by the web layer of the 3x-ui panel.
package entity package entity
import ( import (
@ -10,61 +11,73 @@ import (
"github.com/mhsanaei/3x-ui/v2/util/common" "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 { type Msg struct {
Success bool `json:"success"` Success bool `json:"success"` // Indicates if the operation was successful
Msg string `json:"msg"` Msg string `json:"msg"` // Response message text
Obj any `json:"obj"` 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 { type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"` // Web server settings
WebDomain string `json:"webDomain" form:"webDomain"` WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address
WebPort int `json:"webPort" form:"webPort"` WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation
WebCertFile string `json:"webCertFile" form:"webCertFile"` WebPort int `json:"webPort" form:"webPort"` // Web server port number
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server
WebBasePath string `json:"webBasePath" form:"webBasePath"` WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs
PageSize int `json:"pageSize" form:"pageSize"` SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` // Session maximum age in minutes
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` // UI settings
RemarkModel string `json:"remarkModel" form:"remarkModel"` PageSize int `json:"pageSize" form:"pageSize"` // Number of items per page in lists
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` ExpireDiff int `json:"expireDiff" form:"expireDiff"` // Expiration warning threshold in days
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` // Traffic warning threshold percentage
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Telegram bot settings
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token
TgCpu int `json:"tgCpu" form:"tgCpu"` TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot
TgLang string `json:"tgLang" form:"tgLang"` TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot
TimeLocation string `json:"timeLocation" form:"timeLocation"` TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram
SubEnable bool `json:"subEnable" form:"subEnable"` TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` TgCpu int `json:"tgCpu" form:"tgCpu"` // CPU usage threshold for alerts
SubTitle string `json:"subTitle" form:"subTitle"` TgLang string `json:"tgLang" form:"tgLang"` // Telegram bot language
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"` // Security settings
SubPath string `json:"subPath" form:"subPath"` TimeLocation string `json:"timeLocation" form:"timeLocation"` // Time zone location
SubDomain string `json:"subDomain" form:"subDomain"` TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` // Enable two-factor authentication
SubCertFile string `json:"subCertFile" form:"subCertFile"` TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` // Two-factor authentication token
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"` // Subscription server settings
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` SubEnable bool `json:"subEnable" form:"subEnable"` // Enable subscription server
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"` SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` // Enable JSON subscription endpoint
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` SubTitle string `json:"subTitle" form:"subTitle"` // Subscription title
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` SubListen string `json:"subListen" form:"subListen"` // Subscription server listen IP
SubURI string `json:"subURI" form:"subURI"` SubPort int `json:"subPort" form:"subPort"` // Subscription server port
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"` SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"` SubDomain string `json:"subDomain" form:"subDomain"` // Domain for subscription server validation
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` SubCertFile string `json:"subCertFile" form:"subCertFile"` // SSL certificate file for subscription server
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` // SSL private key file for subscription server
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` SubUpdates int `json:"subUpdates" form:"subUpdates"` // Subscription update interval in minutes
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` // Enable external traffic reporting
Datepicker string `json:"datepicker" form:"datepicker"` 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 { func (s *AllSetting) CheckValid() error {
if s.WebListen != "" { if s.WebListen != "" {
ip := net.ParseIP(s.WebListen) ip := net.ParseIP(s.WebListen)

View file

@ -1,3 +1,4 @@
// Package global provides global variables and interfaces for accessing web and subscription servers.
package global package global
import ( import (
@ -12,27 +13,33 @@ var (
subServer SubServer subServer SubServer
) )
// WebServer interface defines methods for accessing the web server instance.
type WebServer interface { type WebServer interface {
GetCron() *cron.Cron GetCron() *cron.Cron // Get the cron scheduler
GetCtx() context.Context GetCtx() context.Context // Get the server context
} }
// SubServer interface defines methods for accessing the subscription server instance.
type SubServer interface { type SubServer interface {
GetCtx() context.Context GetCtx() context.Context // Get the server context
} }
// SetWebServer sets the global web server instance.
func SetWebServer(s WebServer) { func SetWebServer(s WebServer) {
webServer = s webServer = s
} }
// GetWebServer returns the global web server instance.
func GetWebServer() WebServer { func GetWebServer() WebServer {
return webServer return webServer
} }
// SetSubServer sets the global subscription server instance.
func SetSubServer(s SubServer) { func SetSubServer(s SubServer) {
subServer = s subServer = s
} }
// GetSubServer returns the global subscription server instance.
func GetSubServer() SubServer { func GetSubServer() SubServer {
return subServer return subServer
} }

View file

@ -8,18 +8,21 @@ import (
"time" "time"
) )
// HashEntry represents a stored hash entry with its value and timestamp.
type HashEntry struct { type HashEntry struct {
Hash string Hash string // MD5 hash string
Value string Value string // Original value
Timestamp time.Time Timestamp time.Time // Time when the hash was created
} }
// HashStorage provides thread-safe storage for hash-value pairs with expiration.
type HashStorage struct { type HashStorage struct {
sync.RWMutex sync.RWMutex
Data map[string]HashEntry Data map[string]HashEntry // Map of hash to entry
Expiration time.Duration Expiration time.Duration // Expiration duration for entries
} }
// NewHashStorage creates a new HashStorage instance with the specified expiration duration.
func NewHashStorage(expiration time.Duration) *HashStorage { func NewHashStorage(expiration time.Duration) *HashStorage {
return &HashStorage{ return &HashStorage{
Data: make(map[string]HashEntry), 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 { func (h *HashStorage) SaveHash(query string) string {
h.Lock() h.Lock()
defer h.Unlock() defer h.Unlock()
@ -45,6 +49,7 @@ func (h *HashStorage) SaveHash(query string) string {
return md5HashString return md5HashString
} }
// GetValue retrieves the original value for the given hash, returning true if found.
func (h *HashStorage) GetValue(hash string) (string, bool) { func (h *HashStorage) GetValue(hash string) (string, bool) {
h.RLock() h.RLock()
defer h.RUnlock() defer h.RUnlock()
@ -54,11 +59,13 @@ func (h *HashStorage) GetValue(hash string) (string, bool) {
return entry.Value, exists return entry.Value, exists
} }
// IsMD5 checks if the given string is a valid 32-character MD5 hash.
func (h *HashStorage) IsMD5(hash string) bool { func (h *HashStorage) IsMD5(hash string) bool {
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash) match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
return match return match
} }
// RemoveExpiredHashes removes all hash entries that have exceeded the expiration duration.
func (h *HashStorage) RemoveExpiredHashes() { func (h *HashStorage) RemoveExpiredHashes() {
h.Lock() h.Lock()
defer h.Unlock() defer h.Unlock()
@ -72,6 +79,7 @@ func (h *HashStorage) RemoveExpiredHashes() {
} }
} }
// Reset clears all stored hash entries.
func (h *HashStorage) Reset() { func (h *HashStorage) Reset() {
h.Lock() h.Lock()
defer h.Unlock() defer h.Unlock()

View file

@ -18,6 +18,7 @@ import (
"github.com/mhsanaei/3x-ui/v2/xray" "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 { type CheckClientIpJob struct {
lastClear int64 lastClear int64
disAllowedIps []string disAllowedIps []string
@ -25,6 +26,7 @@ type CheckClientIpJob struct {
var job *CheckClientIpJob var job *CheckClientIpJob
// NewCheckClientIpJob creates a new client IP monitoring job instance.
func NewCheckClientIpJob() *CheckClientIpJob { func NewCheckClientIpJob() *CheckClientIpJob {
job = new(CheckClientIpJob) job = new(CheckClientIpJob)
return job return job

View file

@ -9,16 +9,18 @@ import (
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
) )
// CheckCpuJob monitors CPU usage and sends Telegram notifications when usage exceeds the configured threshold.
type CheckCpuJob struct { type CheckCpuJob struct {
tgbotService service.Tgbot tgbotService service.Tgbot
settingService service.SettingService settingService service.SettingService
} }
// NewCheckCpuJob creates a new CPU monitoring job instance.
func NewCheckCpuJob() *CheckCpuJob { func NewCheckCpuJob() *CheckCpuJob {
return new(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() { func (j *CheckCpuJob) Run() {
threshold, _ := j.settingService.GetTgCpu() threshold, _ := j.settingService.GetTgCpu()

View file

@ -4,15 +4,17 @@ import (
"github.com/mhsanaei/3x-ui/v2/web/service" "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 { type CheckHashStorageJob struct {
tgbotService service.Tgbot tgbotService service.Tgbot
} }
// NewCheckHashStorageJob creates a new hash storage cleanup job instance.
func NewCheckHashStorageJob() *CheckHashStorageJob { func NewCheckHashStorageJob() *CheckHashStorageJob {
return new(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() { func (j *CheckHashStorageJob) Run() {
// Remove expired hashes from storage // Remove expired hashes from storage
j.tgbotService.GetHashStorage().RemoveExpiredHashes() j.tgbotService.GetHashStorage().RemoveExpiredHashes()

View file

@ -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 package job
import ( import (
@ -5,16 +7,18 @@ import (
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
) )
// CheckXrayRunningJob monitors Xray process health and restarts it if it crashes.
type CheckXrayRunningJob struct { type CheckXrayRunningJob struct {
xrayService service.XrayService xrayService service.XrayService
checkTime int
checkTime int
} }
// NewCheckXrayRunningJob creates a new Xray health check job instance.
func NewCheckXrayRunningJob() *CheckXrayRunningJob { func NewCheckXrayRunningJob() *CheckXrayRunningJob {
return new(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() { func (j *CheckXrayRunningJob) Run() {
if !j.xrayService.DidXrayCrash() { if !j.xrayService.DidXrayCrash() {
j.checkTime = 0 j.checkTime = 0

View file

@ -9,8 +9,10 @@ import (
"github.com/mhsanaei/3x-ui/v2/xray" "github.com/mhsanaei/3x-ui/v2/xray"
) )
// ClearLogsJob clears old log files to prevent disk space issues.
type ClearLogsJob struct{} type ClearLogsJob struct{}
// NewClearLogsJob creates a new log cleanup job instance.
func NewClearLogsJob() *ClearLogsJob { func NewClearLogsJob() *ClearLogsJob {
return new(ClearLogsJob) return new(ClearLogsJob)
} }

View file

@ -5,19 +5,23 @@ import (
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
) )
// Period represents the time period for traffic resets.
type Period string type Period string
// PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period.
type PeriodicTrafficResetJob struct { type PeriodicTrafficResetJob struct {
inboundService service.InboundService inboundService service.InboundService
period Period period Period
} }
// NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period.
func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob { func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
return &PeriodicTrafficResetJob{ return &PeriodicTrafficResetJob{
period: period, period: period,
} }
} }
// Run resets traffic statistics for all inbounds that match the configured reset period.
func (j *PeriodicTrafficResetJob) Run() { func (j *PeriodicTrafficResetJob) Run() {
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period)) inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
if err != nil { if err != nil {

View file

@ -4,23 +4,26 @@ import (
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
) )
// LoginStatus represents the status of a login attempt.
type LoginStatus byte type LoginStatus byte
const ( const (
LoginSuccess LoginStatus = 1 LoginSuccess LoginStatus = 1 // Successful login
LoginFail LoginStatus = 0 LoginFail LoginStatus = 0 // Failed login attempt
) )
// StatsNotifyJob sends periodic statistics reports via Telegram bot.
type StatsNotifyJob struct { type StatsNotifyJob struct {
xrayService service.XrayService xrayService service.XrayService
tgbotService service.Tgbot tgbotService service.Tgbot
} }
// NewStatsNotifyJob creates a new statistics notification job instance.
func NewStatsNotifyJob() *StatsNotifyJob { func NewStatsNotifyJob() *StatsNotifyJob {
return new(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() { func (j *StatsNotifyJob) Run() {
if !j.xrayService.IsXrayRunning() { if !j.xrayService.IsXrayRunning() {
return return

View file

@ -10,6 +10,7 @@ import (
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
// XrayTrafficJob collects and processes traffic statistics from Xray, updating the database and optionally informing external APIs.
type XrayTrafficJob struct { type XrayTrafficJob struct {
settingService service.SettingService settingService service.SettingService
xrayService service.XrayService xrayService service.XrayService
@ -17,10 +18,12 @@ type XrayTrafficJob struct {
outboundService service.OutboundService outboundService service.OutboundService
} }
// NewXrayTrafficJob creates a new traffic collection job instance.
func NewXrayTrafficJob() *XrayTrafficJob { func NewXrayTrafficJob() *XrayTrafficJob {
return new(XrayTrafficJob) return new(XrayTrafficJob)
} }
// Run collects traffic statistics from Xray and updates the database, triggering restart if needed.
func (j *XrayTrafficJob) Run() { func (j *XrayTrafficJob) Run() {
if !j.xrayService.IsXrayRunning() { if !j.xrayService.IsXrayRunning() {
return return

View file

@ -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 package locale
import ( import (
@ -20,17 +22,20 @@ var (
LocalizerBot *i18n.Localizer LocalizerBot *i18n.Localizer
) )
// I18nType represents the type of interface for internationalization.
type I18nType string type I18nType string
const ( const (
Bot I18nType = "bot" Bot I18nType = "bot" // Bot interface type
Web I18nType = "web" Web I18nType = "web" // Web interface type
) )
// SettingService interface defines methods for accessing locale settings.
type SettingService interface { type SettingService interface {
GetTgLang() (string, error) GetTgLang() (string, error)
} }
// InitLocalizer initializes the internationalization system with embedded translation files.
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
// set default bundle to english // set default bundle to english
i18nBundle = i18n.NewBundle(language.MustParse("en-US")) i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
@ -49,6 +54,7 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
return nil return nil
} }
// createTemplateData creates a template data map from parameters with optional separator.
func createTemplateData(params []string, separator ...string) map[string]any { func createTemplateData(params []string, separator ...string) map[string]any {
var sep string = "==" var sep string = "=="
if len(separator) > 0 { if len(separator) > 0 {
@ -64,6 +70,9 @@ func createTemplateData(params []string, separator ...string) map[string]any {
return templateData 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 { func I18n(i18nType I18nType, key string, params ...string) string {
var localizer *i18n.Localizer var localizer *i18n.Localizer
@ -96,6 +105,7 @@ func I18n(i18nType I18nType, key string, params ...string) string {
return msg return msg
} }
// initTGBotLocalizer initializes the bot localizer with the configured language.
func initTGBotLocalizer(settingService SettingService) error { func initTGBotLocalizer(settingService SettingService) error {
botLang, err := settingService.GetTgLang() botLang, err := settingService.GetTgLang()
if err != nil { if err != nil {
@ -106,6 +116,10 @@ func initTGBotLocalizer(settingService SettingService) error {
return nil 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 { func LocalizerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// Ensure bundle is initialized so creating a Localizer won't panic // 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 { func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
err := fs.WalkDir(i18nFS, "translation", err := fs.WalkDir(i18nFS, "translation",
func(path string, d fs.DirEntry, err error) error { func(path string, d fs.DirEntry, err error) error {

View file

@ -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 package middleware
import ( import (
@ -8,6 +10,10 @@ import (
"github.com/gin-gonic/gin" "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 { func DomainValidatorMiddleware(domain string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
host := c.Request.Host host := c.Request.Host

View file

@ -7,6 +7,9 @@ import (
"github.com/gin-gonic/gin" "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 { func RedirectMiddleware(basePath string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// Redirect from old '/xui' path to '/panel' // Redirect from old '/xui' path to '/panel'

View file

@ -1,3 +1,5 @@
// Package network provides network utilities for the 3x-ui web panel,
// including automatic HTTP to HTTPS redirection functionality.
package network package network
import ( import (
@ -9,6 +11,9 @@ import (
"sync" "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 { type AutoHttpsConn struct {
net.Conn net.Conn
@ -18,6 +23,8 @@ type AutoHttpsConn struct {
readRequestOnce sync.Once 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 { func NewAutoHttpsConn(conn net.Conn) net.Conn {
return &AutoHttpsConn{ return &AutoHttpsConn{
Conn: conn, Conn: conn,
@ -49,6 +56,9 @@ func (c *AutoHttpsConn) readRequest() bool {
return true 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) { func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
c.readRequestOnce.Do(func() { c.readRequestOnce.Do(func() {
c.readRequest() c.readRequest()

View file

@ -2,16 +2,22 @@ package network
import "net" 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 { type AutoHttpsListener struct {
net.Listener 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 { func NewAutoHttpsListener(listener net.Listener) net.Listener {
return &AutoHttpsListener{ return &AutoHttpsListener{
Listener: listener, 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) { func (l *AutoHttpsListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept() conn, err := l.Listener.Accept()
if err != nil { if err != nil {

View file

@ -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 package service
import ( import (
@ -17,10 +19,15 @@ import (
"gorm.io/gorm" "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 { type InboundService struct {
xrayApi xray.XrayAPI 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) { func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
@ -31,6 +38,8 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
return inbounds, nil 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) { func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
@ -163,6 +172,10 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
return "", nil 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) { func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0) exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0)
if err != nil { if err != nil {
@ -269,6 +282,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
return inbound, needRestart, err 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) { func (s *InboundService) DelInbound(id int) (bool, error) {
db := database.GetDB() db := database.GetDB()
@ -322,6 +338,9 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
return inbound, nil 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) { func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id) exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id)
if err != nil { if err != nil {

View file

@ -9,6 +9,8 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// OutboundService provides business logic for managing Xray outbound configurations.
// It handles outbound traffic monitoring and statistics.
type OutboundService struct{} type OutboundService struct{}
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {

View file

@ -8,6 +8,8 @@ import (
"github.com/mhsanaei/3x-ui/v2/logger" "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{} type PanelService struct{}
func (s *PanelService) RestartPanel(delay time.Duration) error { func (s *PanelService) RestartPanel(delay time.Duration) error {

View file

@ -35,14 +35,18 @@ import (
"github.com/shirou/gopsutil/v4/net" "github.com/shirou/gopsutil/v4/net"
) )
// ProcessState represents the current state of a system process.
type ProcessState string type ProcessState string
// Process state constants
const ( const (
Running ProcessState = "running" Running ProcessState = "running" // Process is running normally
Stop ProcessState = "stop" Stop ProcessState = "stop" // Process is stopped
Error ProcessState = "error" 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 { type Status struct {
T time.Time `json:"-"` T time.Time `json:"-"`
Cpu float64 `json:"cpu"` Cpu float64 `json:"cpu"`
@ -89,10 +93,13 @@ type Status struct {
} `json:"appStats"` } `json:"appStats"`
} }
// Release represents information about a software release from GitHub.
type Release struct { 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 { type ServerService struct {
xrayService XrayService xrayService XrayService
inboundService InboundService inboundService InboundService

View file

@ -75,6 +75,8 @@ var defaultValueMap = map[string]string{
"externalTrafficInformURI": "", "externalTrafficInformURI": "",
} }
// SettingService provides business logic for application settings management.
// It handles configuration storage, retrieval, and validation for all system settings.
type SettingService struct{} type SettingService struct{}
func (s *SettingService) GetDefaultJsonConfig() (any, error) { func (s *SettingService) GetDefaultJsonConfig() (any, error) {

View file

@ -65,14 +65,18 @@ var (
var userStates = make(map[int64]string) var userStates = make(map[int64]string)
// LoginStatus represents the result of a login attempt.
type LoginStatus byte type LoginStatus byte
// Login status constants
const ( const (
LoginSuccess LoginStatus = 1 LoginSuccess LoginStatus = 1 // Login was successful
LoginFail LoginStatus = 0 LoginFail LoginStatus = 0 // Login failed
EmptyTelegramUserID = int64(0) 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 { type Tgbot struct {
inboundService InboundService inboundService InboundService
settingService SettingService settingService SettingService
@ -81,18 +85,22 @@ type Tgbot struct {
lastStatus *Status lastStatus *Status
} }
// NewTgbot creates a new Tgbot instance.
func (t *Tgbot) NewTgbot() *Tgbot { func (t *Tgbot) NewTgbot() *Tgbot {
return new(Tgbot) return new(Tgbot)
} }
// I18nBot retrieves a localized message for the bot interface.
func (t *Tgbot) I18nBot(name string, params ...string) string { func (t *Tgbot) I18nBot(name string, params ...string) string {
return locale.I18n(locale.Bot, name, params...) return locale.I18n(locale.Bot, name, params...)
} }
// GetHashStorage returns the hash storage instance for callback queries.
func (t *Tgbot) GetHashStorage() *global.HashStorage { func (t *Tgbot) GetHashStorage() *global.HashStorage {
return hashStorage return hashStorage
} }
// Start initializes and starts the Telegram bot with the provided translation files.
func (t *Tgbot) Start(i18nFS embed.FS) error { func (t *Tgbot) Start(i18nFS embed.FS) error {
// Initialize localizer // Initialize localizer
err := locale.InitLocalizer(i18nFS, &t.settingService) err := locale.InitLocalizer(i18nFS, &t.settingService)
@ -173,6 +181,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
return nil 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) { func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
if proxyUrl == "" && apiServerUrl == "" { if proxyUrl == "" && apiServerUrl == "" {
return telego.NewBot(token) 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)) return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
} }
// IsRunning checks if the Telegram bot is currently running.
func (t *Tgbot) IsRunning() bool { func (t *Tgbot) IsRunning() bool {
return isRunning return isRunning
} }
// SetHostname sets the hostname for the bot.
func (t *Tgbot) SetHostname() { func (t *Tgbot) SetHostname() {
host, err := os.Hostname() host, err := os.Hostname()
if err != nil { if err != nil {
@ -223,6 +234,7 @@ func (t *Tgbot) SetHostname() {
hostname = host hostname = host
} }
// Stop stops the Telegram bot and cleans up resources.
func (t *Tgbot) Stop() { func (t *Tgbot) Stop() {
if botHandler != nil { if botHandler != nil {
botHandler.Stop() botHandler.Stop()
@ -232,6 +244,7 @@ func (t *Tgbot) Stop() {
adminIds = nil adminIds = nil
} }
// encodeQuery encodes the query string if it's longer than 64 characters.
func (t *Tgbot) encodeQuery(query string) string { func (t *Tgbot) encodeQuery(query string) string {
// NOTE: we only need to hash for more than 64 chars // NOTE: we only need to hash for more than 64 chars
if len(query) <= 64 { if len(query) <= 64 {
@ -241,6 +254,7 @@ func (t *Tgbot) encodeQuery(query string) string {
return hashStorage.SaveHash(query) return hashStorage.SaveHash(query)
} }
// decodeQuery decodes a hashed query string back to its original form.
func (t *Tgbot) decodeQuery(query string) (string, error) { func (t *Tgbot) decodeQuery(query string) (string, error) {
if !hashStorage.IsMD5(query) { if !hashStorage.IsMD5(query) {
return query, nil return query, nil
@ -254,6 +268,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
return decoded, nil return decoded, nil
} }
// OnReceive starts the message receiving loop for the Telegram bot.
func (t *Tgbot) OnReceive() { func (t *Tgbot) OnReceive() {
params := telego.GetUpdatesParams{ params := telego.GetUpdatesParams{
Timeout: 10, Timeout: 10,
@ -430,6 +445,7 @@ func (t *Tgbot) OnReceive() {
botHandler.Start() botHandler.Start()
} }
// answerCommand processes incoming command messages from Telegram users.
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) { func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
msg, onlyMessage := "", false 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) { func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) {
if onlyMessage { if onlyMessage {
t.SendMsgToTgbot(chatId, msg) 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 { func (t *Tgbot) randomLowerAndNum(length int) string {
charset := "abcdefghijklmnopqrstuvwxyz0123456789" charset := "abcdefghijklmnopqrstuvwxyz0123456789"
bytes := make([]byte, length) bytes := make([]byte, length)
@ -524,6 +541,7 @@ func (t *Tgbot) randomLowerAndNum(length int) string {
return string(bytes) return string(bytes)
} }
// randomShadowSocksPassword generates a random password for Shadowsocks.
func (t *Tgbot) randomShadowSocksPassword() string { func (t *Tgbot) randomShadowSocksPassword() string {
array := make([]byte, 32) array := make([]byte, 32)
_, err := rand.Read(array) _, err := rand.Read(array)
@ -533,6 +551,7 @@ func (t *Tgbot) randomShadowSocksPassword() string {
return base64.StdEncoding.EncodeToString(array) return base64.StdEncoding.EncodeToString(array)
} }
// answerCallback processes callback queries from inline keyboards.
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) { func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
chatId := callbackQuery.Message.GetChat().ID 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) { func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) {
var message string var message string
@ -1864,6 +1884,7 @@ func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol mo
return message, nil return message, nil
} }
// BuildJSONForProtocol builds a JSON string for the given protocol with client data.
func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) { func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
var jsonString string var jsonString string
@ -1942,6 +1963,7 @@ func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
return jsonString, nil return jsonString, nil
} }
// SubmitAddClient submits the client addition request to the inbound service.
func (t *Tgbot) SubmitAddClient() (bool, error) { func (t *Tgbot) SubmitAddClient() (bool, error) {
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
@ -1964,6 +1986,7 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
return t.inboundService.AddInboundClient(newInbound) return t.inboundService.AddInboundClient(newInbound)
} }
// checkAdmin checks if the given Telegram ID is an admin.
func checkAdmin(tgId int64) bool { func checkAdmin(tgId int64) bool {
for _, adminId := range adminIds { for _, adminId := range adminIds {
if adminId == tgId { if adminId == tgId {
@ -1973,6 +1996,7 @@ func checkAdmin(tgId int64) bool {
return false 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) { func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
numericKeyboard := tu.InlineKeyboard( numericKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@ -2028,6 +2052,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
t.SendMsgToTgbot(chatId, msg, ReplyMarkup) 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) { func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {
if !isRunning { if !isRunning {
return return
@ -2143,6 +2168,7 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
return subURL, subJsonURL, nil return subURL, subJsonURL, nil
} }
// sendClientSubLinks sends the subscription links for the client to the chat.
func (t *Tgbot) sendClientSubLinks(chatId int64, email string) { func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
subURL, subJsonURL, err := t.buildSubscriptionURLs(email) subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
if err != nil { 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) { func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
if len(replyMarkup) > 0 { if len(replyMarkup) > 0 {
for _, adminId := range adminIds { 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() { func (t *Tgbot) SendReport() {
runTime, err := t.settingService.GetTgbotRuntime() runTime, err := t.settingService.GetTgbotRuntime()
if err == nil && len(runTime) > 0 { 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() { func (t *Tgbot) SendBackupToAdmins() {
if !t.IsRunning() { if !t.IsRunning() {
return return
@ -2380,6 +2409,7 @@ func (t *Tgbot) SendBackupToAdmins() {
} }
} }
// sendExhaustedToAdmins sends notifications about exhausted clients to admins.
func (t *Tgbot) sendExhaustedToAdmins() { func (t *Tgbot) sendExhaustedToAdmins() {
if !t.IsRunning() { if !t.IsRunning() {
return 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 { func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string {
info := t.prepareServerUsageInfo() info := t.prepareServerUsageInfo()
@ -2410,6 +2441,7 @@ func (t *Tgbot) sendServerUsage() string {
return info return info
} }
// prepareServerUsageInfo prepares the server usage information string.
func (t *Tgbot) prepareServerUsageInfo() string { func (t *Tgbot) prepareServerUsageInfo() string {
info, ipv4, ipv6 := "", "", "" info, ipv4, ipv6 := "", "", ""
@ -2459,6 +2491,7 @@ func (t *Tgbot) prepareServerUsageInfo() string {
return info 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) { func (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) {
if !t.IsRunning() { if !t.IsRunning() {
return return
@ -2490,6 +2523,7 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
t.SendMsgToTgbotAdmins(msg) t.SendMsgToTgbotAdmins(msg)
} }
// getInboundUsages retrieves and formats inbound usage information.
func (t *Tgbot) getInboundUsages() string { func (t *Tgbot) getInboundUsages() string {
info := "" info := ""
// get traffic // get traffic
@ -2515,6 +2549,8 @@ func (t *Tgbot) getInboundUsages() string {
} }
return info return info
} }
// getInbounds creates an inline keyboard with all inbounds.
func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) { func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
inbounds, err := t.inboundService.GetAllInbounds() inbounds, err := t.inboundService.GetAllInbounds()
if err != nil { if err != nil {
@ -2546,8 +2582,7 @@ func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
return keyboard, nil return keyboard, nil
} }
// getInboundsFor builds an inline keyboard of inbounds where each button leads to a custom next action // getInboundsFor builds an inline keyboard of inbounds for a custom next action.
// nextAction should be one of: get_clients_for_sub|get_clients_for_individual|get_clients_for_qr
func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) { func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) {
inbounds, err := t.inboundService.GetAllInbounds() inbounds, err := t.inboundService.GetAllInbounds()
if err != nil { if err != nil {
@ -2614,6 +2649,7 @@ func (t *Tgbot) getInboundClientsFor(inboundID int, action string) (*telego.Inli
return keyboard, nil return keyboard, nil
} }
// getInboundsAddClient creates an inline keyboard for adding clients to inbounds.
func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) { func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
inbounds, err := t.inboundService.GetAllInbounds() inbounds, err := t.inboundService.GetAllInbounds()
if err != nil { if err != nil {
@ -2656,6 +2692,7 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
return keyboard, nil return keyboard, nil
} }
// getInboundClients creates an inline keyboard with clients of a specific inbound.
func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) { func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {
inbound, err := t.inboundService.GetInbound(id) inbound, err := t.inboundService.GetInbound(id)
if err != nil { if err != nil {
@ -2690,6 +2727,7 @@ func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error)
return keyboard, nil return keyboard, nil
} }
// clientInfoMsg formats client information message based on traffic and flags.
func (t *Tgbot) clientInfoMsg( func (t *Tgbot) clientInfoMsg(
traffic *xray.ClientTraffic, traffic *xray.ClientTraffic,
printEnabled bool, printEnabled bool,
@ -2796,6 +2834,7 @@ func (t *Tgbot) clientInfoMsg(
return output return output
} }
// getClientUsage retrieves and sends client usage information to the chat.
func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) { func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID) traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
if err != nil { if err != nil {
@ -2838,6 +2877,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
t.SendAnswer(chatId, output, false) 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) { func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
ips, err := t.inboundService.GetInboundClientIps(email) ips, err := t.inboundService.GetInboundClientIps(email)
if err != nil || len(ips) == 0 { 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) { func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) {
traffic, client, err := t.inboundService.GetClientByEmail(email) traffic, client, err := t.inboundService.GetClientByEmail(email)
if err != nil { 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) { func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email) traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil { 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) { func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
if err != nil { 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) { func (t *Tgbot) searchInbound(chatId int64, remark string) {
inbounds, err := t.inboundService.SearchInbounds(remark) inbounds, err := t.inboundService.SearchInbounds(remark)
if err != nil { 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) { func (t *Tgbot) getExhausted(chatId int64) {
trDiff := int64(0) trDiff := int64(0)
exDiff := 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() { func (t *Tgbot) notifyExhausted() {
trDiff := int64(0) trDiff := int64(0)
exDiff := 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 { func int64Contains(slice []int64, item int64) bool {
for _, s := range slice { for _, s := range slice {
if s == item { if s == item {
@ -3271,6 +3318,7 @@ func int64Contains(slice []int64, item int64) bool {
return false return false
} }
// onlineClients retrieves and sends information about online clients.
func (t *Tgbot) onlineClients(chatId int64, messageID ...int) { func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
if !p.IsRunning() { if !p.IsRunning() {
return 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) { func (t *Tgbot) sendBackup(chatId int64) {
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05")) output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
t.SendMsgToTgbot(chatId, output) 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) { func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
if dt { if dt {
output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05")) 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) { func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
params := telego.AnswerCallbackQueryParams{ params := telego.AnswerCallbackQueryParams{
CallbackQueryID: id, 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) { func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {
params := telego.EditMessageReplyMarkupParams{ params := telego.EditMessageReplyMarkupParams{
ChatID: tu.ID(chatId), 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) { func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) {
params := telego.EditMessageTextParams{ params := telego.EditMessageTextParams{
ChatID: tu.ID(chatId), 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) { func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) {
// Determine if replyMarkup was passed; otherwise, set it to nil // Determine if replyMarkup was passed; otherwise, set it to nil
var replyMarkupParam telego.ReplyMarkup 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) { func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
params := telego.DeleteMessageParams{ params := telego.DeleteMessageParams{
ChatID: tu.ID(chatId), 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 { func (t *Tgbot) isSingleWord(text string) bool {
text = strings.TrimSpace(text) text = strings.TrimSpace(text)
re := regexp.MustCompile(`\s+`) re := regexp.MustCompile(`\s+`)

View file

@ -12,10 +12,14 @@ import (
"gorm.io/gorm" "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 { type UserService struct {
settingService SettingService 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) { func (s *UserService) GetFirstUser() (*model.User, error) {
db := database.GetDB() db := database.GetDB()

View file

@ -12,6 +12,8 @@ import (
"github.com/mhsanaei/3x-ui/v2/util/common" "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 { type WarpService struct {
SettingService SettingService
} }

View file

@ -20,16 +20,20 @@ var (
result string result string
) )
// XrayService provides business logic for Xray process management.
// It handles starting, stopping, restarting Xray, and managing its configuration.
type XrayService struct { type XrayService struct {
inboundService InboundService inboundService InboundService
settingService SettingService settingService SettingService
xrayAPI xray.XrayAPI xrayAPI xray.XrayAPI
} }
// IsXrayRunning checks if the Xray process is currently running.
func (s *XrayService) IsXrayRunning() bool { func (s *XrayService) IsXrayRunning() bool {
return p != nil && p.IsRunning() return p != nil && p.IsRunning()
} }
// GetXrayErr returns the error from the Xray process, if any.
func (s *XrayService) GetXrayErr() error { func (s *XrayService) GetXrayErr() error {
if p == nil { if p == nil {
return nil return nil
@ -46,6 +50,7 @@ func (s *XrayService) GetXrayErr() error {
return err return err
} }
// GetXrayResult returns the result string from the Xray process.
func (s *XrayService) GetXrayResult() string { func (s *XrayService) GetXrayResult() string {
if result != "" { if result != "" {
return result return result
@ -68,6 +73,7 @@ func (s *XrayService) GetXrayResult() string {
return result return result
} }
// GetXrayVersion returns the version of the running Xray process.
func (s *XrayService) GetXrayVersion() string { func (s *XrayService) GetXrayVersion() string {
if p == nil { if p == nil {
return "Unknown" return "Unknown"
@ -75,10 +81,13 @@ func (s *XrayService) GetXrayVersion() string {
return p.GetVersion() 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 { func RemoveIndex(s []any, index int) []any {
return append(s[:index], s[index+1:]...) 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) { func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
templateConfig, err := s.settingService.GetXrayConfigTemplate() templateConfig, err := s.settingService.GetXrayConfigTemplate()
if err != nil { if err != nil {
@ -182,6 +191,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
return xrayConfig, nil return xrayConfig, nil
} }
// GetXrayTraffic fetches the current traffic statistics from the running Xray process.
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) { func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
if !s.IsXrayRunning() { if !s.IsXrayRunning() {
err := errors.New("xray is not running") err := errors.New("xray is not running")
@ -200,6 +210,7 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
return traffic, clientTraffic, nil return traffic, clientTraffic, nil
} }
// RestartXray restarts the Xray process, optionally forcing a restart even if config unchanged.
func (s *XrayService) RestartXray(isForce bool) error { func (s *XrayService) RestartXray(isForce bool) error {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
@ -229,6 +240,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
return nil return nil
} }
// StopXray stops the running Xray process.
func (s *XrayService) StopXray() error { func (s *XrayService) StopXray() error {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
@ -240,15 +252,17 @@ func (s *XrayService) StopXray() error {
return errors.New("xray is not running") return errors.New("xray is not running")
} }
// SetToNeedRestart marks that Xray needs to be restarted.
func (s *XrayService) SetToNeedRestart() { func (s *XrayService) SetToNeedRestart() {
isNeedXrayRestart.Store(true) isNeedXrayRestart.Store(true)
} }
// IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false.
func (s *XrayService) IsNeedRestartAndSetFalse() bool { func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CompareAndSwap(true, false) 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 { func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load() return !s.IsXrayRunning() && !isManuallyStopped.Load()
} }

View file

@ -8,6 +8,8 @@ import (
"github.com/mhsanaei/3x-ui/v2/xray" "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 { type XraySettingService struct {
SettingService SettingService
} }

View file

@ -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 package session
import ( import (
@ -19,6 +21,8 @@ func init() {
gob.Register(model.User{}) 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) { func SetLoginUser(c *gin.Context, user *model.User) {
if user == nil { if user == nil {
return return
@ -27,6 +31,8 @@ func SetLoginUser(c *gin.Context, user *model.User) {
s.Set(loginUserKey, *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) { func SetMaxAge(c *gin.Context, maxAge int) {
s := sessions.Default(c) s := sessions.Default(c)
s.Options(sessions.Options{ 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 { func GetLoginUser(c *gin.Context) *model.User {
s := sessions.Default(c) s := sessions.Default(c)
obj := s.Get(loginUserKey) obj := s.Get(loginUserKey)
@ -52,10 +60,14 @@ func GetLoginUser(c *gin.Context) *model.User {
return &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 { func IsLogin(c *gin.Context) bool {
return GetLoginUser(c) != nil 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) { func ClearSession(c *gin.Context) {
s := sessions.Default(c) s := sessions.Default(c)
s.Clear() s.Clear()

View file

@ -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 package web
import ( import (
@ -78,15 +80,17 @@ func (f *wrapAssetsFileInfo) ModTime() time.Time {
return startTime 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 { func EmbeddedHTML() embed.FS {
return htmlFS return htmlFS
} }
// EmbeddedAssets returns the embedded assets filesystem for reuse by other servers.
func EmbeddedAssets() embed.FS { func EmbeddedAssets() embed.FS {
return assetsFS return assetsFS
} }
// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs.
type Server struct { type Server struct {
httpServer *http.Server httpServer *http.Server
listener net.Listener listener net.Listener
@ -106,6 +110,7 @@ type Server struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
// NewServer creates a new web server instance with a cancellable context.
func NewServer() *Server { func NewServer() *Server {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &Server{ 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) { func (s *Server) getHtmlFiles() ([]string, error) {
files := make([]string, 0) files := make([]string, 0)
dir, _ := os.Getwd() dir, _ := os.Getwd()
@ -133,6 +140,9 @@ func (s *Server) getHtmlFiles() ([]string, error) {
return files, nil 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) { func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) {
t := template.New("").Funcs(funcMap) t := template.New("").Funcs(funcMap)
err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error { 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 return t, nil
} }
// initRouter initializes Gin, registers middleware, templates, static
// assets, controllers and returns the configured engine.
func (s *Server) initRouter() (*gin.Engine, error) { func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() { if config.IsDebug() {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
@ -259,6 +271,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
return engine, nil 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() { func (s *Server) startTask() {
err := s.xrayService.RestartXray(true) err := s.xrayService.RestartXray(true)
if err != nil { 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) { func (s *Server) Start() (err error) {
// This is an anonymous function, no function name // This is an anonymous function, no function name
defer func() { defer func() {
@ -404,6 +419,7 @@ func (s *Server) Start() (err error) {
return nil return nil
} }
// Stop gracefully shuts down the web server, stops Xray, cron jobs, and Telegram bot.
func (s *Server) Stop() error { func (s *Server) Stop() error {
s.cancel() s.cancel()
s.xrayService.StopXray() s.xrayService.StopXray()
@ -424,10 +440,12 @@ func (s *Server) Stop() error {
return common.Combine(err1, err2) return common.Combine(err1, err2)
} }
// GetCtx returns the server's context for cancellation and deadline management.
func (s *Server) GetCtx() context.Context { func (s *Server) GetCtx() context.Context {
return s.ctx return s.ctx
} }
// GetCron returns the server's cron scheduler instance.
func (s *Server) GetCron() *cron.Cron { func (s *Server) GetCron() *cron.Cron {
return s.cron return s.cron
} }

View file

@ -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 package xray
import ( import (
@ -25,6 +28,7 @@ import (
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
) )
// XrayAPI is a gRPC client for managing Xray core configuration, inbounds, outbounds, and statistics.
type XrayAPI struct { type XrayAPI struct {
HandlerServiceClient *command.HandlerServiceClient HandlerServiceClient *command.HandlerServiceClient
StatsServiceClient *statsService.StatsServiceClient StatsServiceClient *statsService.StatsServiceClient
@ -32,6 +36,7 @@ type XrayAPI struct {
isConnected bool isConnected bool
} }
// Init connects to the Xray API server and initializes handler and stats service clients.
func (x *XrayAPI) Init(apiPort int) error { func (x *XrayAPI) Init(apiPort int) error {
if apiPort <= 0 || apiPort > math.MaxUint16 { if apiPort <= 0 || apiPort > math.MaxUint16 {
return fmt.Errorf("invalid Xray API port: %d", apiPort) return fmt.Errorf("invalid Xray API port: %d", apiPort)
@ -55,6 +60,7 @@ func (x *XrayAPI) Init(apiPort int) error {
return nil return nil
} }
// Close closes the gRPC connection and resets the XrayAPI client state.
func (x *XrayAPI) Close() { func (x *XrayAPI) Close() {
if x.grpcClient != nil { if x.grpcClient != nil {
x.grpcClient.Close() x.grpcClient.Close()
@ -64,6 +70,7 @@ func (x *XrayAPI) Close() {
x.isConnected = false x.isConnected = false
} }
// AddInbound adds a new inbound configuration to the Xray core via gRPC.
func (x *XrayAPI) AddInbound(inbound []byte) error { func (x *XrayAPI) AddInbound(inbound []byte) error {
client := *x.HandlerServiceClient client := *x.HandlerServiceClient
@ -85,6 +92,7 @@ func (x *XrayAPI) AddInbound(inbound []byte) error {
return err return err
} }
// DelInbound removes an inbound configuration from the Xray core by tag.
func (x *XrayAPI) DelInbound(tag string) error { func (x *XrayAPI) DelInbound(tag string) error {
client := *x.HandlerServiceClient client := *x.HandlerServiceClient
_, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{ _, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{
@ -93,6 +101,7 @@ func (x *XrayAPI) DelInbound(tag string) error {
return err 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 { func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error {
var account *serial.TypedMessage var account *serial.TypedMessage
switch Protocol { switch Protocol {
@ -153,6 +162,7 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
return err return err
} }
// RemoveUser removes a user from an inbound in the Xray core by email.
func (x *XrayAPI) RemoveUser(inboundTag, email string) error { func (x *XrayAPI) RemoveUser(inboundTag, email string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@ -171,6 +181,7 @@ func (x *XrayAPI) RemoveUser(inboundTag, email string) error {
return nil return nil
} }
// GetTraffic queries traffic statistics from the Xray core, optionally resetting counters.
func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
if x.grpcClient == nil { if x.grpcClient == nil {
return nil, nil, common.NewError("xray api is not initialized") 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 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) { func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) {
isInbound := matches[1] == "inbound" isInbound := matches[1] == "inbound"
tag := matches[2] 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) { func processClientTraffic(matches []string, value int64, clientTrafficMap map[string]*ClientTraffic) {
email := matches[1] email := matches[1]
isDown := matches[2] == "downlink" 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 { func mapToSlice[T any](m map[string]*T) []*T {
result := make([]*T, 0, len(m)) result := make([]*T, 0, len(m))
for _, v := range m { for _, v := range m {

View file

@ -1,5 +1,7 @@
package xray 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 { type ClientTraffic struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
InboundId int `json:"inboundId" form:"inboundId"` InboundId int `json:"inboundId" form:"inboundId"`

View file

@ -6,6 +6,8 @@ import (
"github.com/mhsanaei/3x-ui/v2/util/json_util" "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 { type Config struct {
LogConfig json_util.RawMessage `json:"log"` LogConfig json_util.RawMessage `json:"log"`
RouterConfig json_util.RawMessage `json:"routing"` RouterConfig json_util.RawMessage `json:"routing"`
@ -23,6 +25,7 @@ type Config struct {
Metrics json_util.RawMessage `json:"metrics"` Metrics json_util.RawMessage `json:"metrics"`
} }
// Equals compares two Config instances for deep equality.
func (c *Config) Equals(other *Config) bool { func (c *Config) Equals(other *Config) bool {
if len(c.InboundConfigs) != len(other.InboundConfigs) { if len(c.InboundConfigs) != len(other.InboundConfigs) {
return false return false

View file

@ -6,6 +6,8 @@ import (
"github.com/mhsanaei/3x-ui/v2/util/json_util" "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 { type InboundConfig struct {
Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string
Port int `json:"port"` Port int `json:"port"`
@ -16,6 +18,7 @@ type InboundConfig struct {
Sniffing json_util.RawMessage `json:"sniffing"` Sniffing json_util.RawMessage `json:"sniffing"`
} }
// Equals compares two InboundConfig instances for deep equality.
func (c *InboundConfig) Equals(other *InboundConfig) bool { func (c *InboundConfig) Equals(other *InboundConfig) bool {
if !bytes.Equal(c.Listen, other.Listen) { if !bytes.Equal(c.Listen, other.Listen) {
return false return false

View file

@ -8,14 +8,17 @@ import (
"github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/logger"
) )
// NewLogWriter returns a new LogWriter for processing Xray log output.
func NewLogWriter() *LogWriter { func NewLogWriter() *LogWriter {
return &LogWriter{} return &LogWriter{}
} }
// LogWriter processes and filters log output from the Xray process, handling crash detection and message filtering.
type LogWriter struct { type LogWriter struct {
lastLine string 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) { func (lw *LogWriter) Write(m []byte) (n int, err error) {
crashRegex := regexp.MustCompile(`(?i)(panic|exception|stack trace|fatal error)`) crashRegex := regexp.MustCompile(`(?i)(panic|exception|stack trace|fatal error)`)

View file

@ -18,46 +18,57 @@ import (
"github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/util/common"
) )
// GetBinaryName returns the Xray binary filename for the current OS and architecture.
func GetBinaryName() string { func GetBinaryName() string {
return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH) return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
} }
// GetBinaryPath returns the full path to the Xray binary executable.
func GetBinaryPath() string { func GetBinaryPath() string {
return config.GetBinFolderPath() + "/" + GetBinaryName() return config.GetBinFolderPath() + "/" + GetBinaryName()
} }
// GetConfigPath returns the path to the Xray configuration file in the binary folder.
func GetConfigPath() string { func GetConfigPath() string {
return config.GetBinFolderPath() + "/config.json" return config.GetBinFolderPath() + "/config.json"
} }
// GetGeositePath returns the path to the geosite data file used by Xray.
func GetGeositePath() string { func GetGeositePath() string {
return config.GetBinFolderPath() + "/geosite.dat" return config.GetBinFolderPath() + "/geosite.dat"
} }
// GetGeoipPath returns the path to the geoip data file used by Xray.
func GetGeoipPath() string { func GetGeoipPath() string {
return config.GetBinFolderPath() + "/geoip.dat" return config.GetBinFolderPath() + "/geoip.dat"
} }
// GetIPLimitLogPath returns the path to the IP limit log file.
func GetIPLimitLogPath() string { func GetIPLimitLogPath() string {
return config.GetLogFolder() + "/3xipl.log" return config.GetLogFolder() + "/3xipl.log"
} }
// GetIPLimitBannedLogPath returns the path to the banned IP log file.
func GetIPLimitBannedLogPath() string { func GetIPLimitBannedLogPath() string {
return config.GetLogFolder() + "/3xipl-banned.log" return config.GetLogFolder() + "/3xipl-banned.log"
} }
// GetIPLimitBannedPrevLogPath returns the path to the previous banned IP log file.
func GetIPLimitBannedPrevLogPath() string { func GetIPLimitBannedPrevLogPath() string {
return config.GetLogFolder() + "/3xipl-banned.prev.log" return config.GetLogFolder() + "/3xipl-banned.prev.log"
} }
// GetAccessPersistentLogPath returns the path to the persistent access log file.
func GetAccessPersistentLogPath() string { func GetAccessPersistentLogPath() string {
return config.GetLogFolder() + "/3xipl-ap.log" return config.GetLogFolder() + "/3xipl-ap.log"
} }
// GetAccessPersistentPrevLogPath returns the path to the previous persistent access log file.
func GetAccessPersistentPrevLogPath() string { func GetAccessPersistentPrevLogPath() string {
return config.GetLogFolder() + "/3xipl-ap.prev.log" return config.GetLogFolder() + "/3xipl-ap.prev.log"
} }
// GetAccessLogPath reads the Xray config and returns the access log file path.
func GetAccessLogPath() (string, error) { func GetAccessLogPath() (string, error) {
config, err := os.ReadFile(GetConfigPath()) config, err := os.ReadFile(GetConfigPath())
if err != nil { if err != nil {
@ -82,14 +93,17 @@ func GetAccessLogPath() (string, error) {
return "", err return "", err
} }
// stopProcess calls Stop on the given Process instance.
func stopProcess(p *Process) { func stopProcess(p *Process) {
p.Stop() p.Stop()
} }
// Process wraps an Xray process instance and provides management methods.
type Process struct { type Process struct {
*process *process
} }
// NewProcess creates a new Xray process and sets up cleanup on garbage collection.
func NewProcess(xrayConfig *Config) *Process { func NewProcess(xrayConfig *Config) *Process {
p := &Process{newProcess(xrayConfig)} p := &Process{newProcess(xrayConfig)}
runtime.SetFinalizer(p, stopProcess) runtime.SetFinalizer(p, stopProcess)
@ -110,6 +124,7 @@ type process struct {
startTime time.Time startTime time.Time
} }
// newProcess creates a new internal process struct for Xray.
func newProcess(config *Config) *process { func newProcess(config *Config) *process {
return &process{ return &process{
version: "Unknown", 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 { func (p *process) IsRunning() bool {
if p.cmd == nil || p.cmd.Process == nil { if p.cmd == nil || p.cmd.Process == nil {
return false return false
@ -129,10 +145,12 @@ func (p *process) IsRunning() bool {
return false return false
} }
// GetErr returns the last error encountered by the Xray process.
func (p *process) GetErr() error { func (p *process) GetErr() error {
return p.exitErr return p.exitErr
} }
// GetResult returns the last log line or error from the Xray process.
func (p *process) GetResult() string { func (p *process) GetResult() string {
if len(p.logWriter.lastLine) == 0 && p.exitErr != nil { if len(p.logWriter.lastLine) == 0 && p.exitErr != nil {
return p.exitErr.Error() return p.exitErr.Error()
@ -140,30 +158,37 @@ func (p *process) GetResult() string {
return p.logWriter.lastLine return p.logWriter.lastLine
} }
// GetVersion returns the version string of the Xray process.
func (p *process) GetVersion() string { func (p *process) GetVersion() string {
return p.version return p.version
} }
// GetAPIPort returns the API port used by the Xray process.
func (p *Process) GetAPIPort() int { func (p *Process) GetAPIPort() int {
return p.apiPort return p.apiPort
} }
// GetConfig returns the configuration used by the Xray process.
func (p *Process) GetConfig() *Config { func (p *Process) GetConfig() *Config {
return p.config return p.config
} }
// GetOnlineClients returns the list of online clients for the Xray process.
func (p *Process) GetOnlineClients() []string { func (p *Process) GetOnlineClients() []string {
return p.onlineClients return p.onlineClients
} }
// SetOnlineClients sets the list of online clients for the Xray process.
func (p *Process) SetOnlineClients(users []string) { func (p *Process) SetOnlineClients(users []string) {
p.onlineClients = users p.onlineClients = users
} }
// GetUptime returns the uptime of the Xray process in seconds.
func (p *Process) GetUptime() uint64 { func (p *Process) GetUptime() uint64 {
return uint64(time.Since(p.startTime).Seconds()) return uint64(time.Since(p.startTime).Seconds())
} }
// refreshAPIPort updates the API port from the inbound configs.
func (p *process) refreshAPIPort() { func (p *process) refreshAPIPort() {
for _, inbound := range p.config.InboundConfigs { for _, inbound := range p.config.InboundConfigs {
if inbound.Tag == "api" { 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() { func (p *process) refreshVersion() {
cmd := exec.Command(GetBinaryPath(), "-version") cmd := exec.Command(GetBinaryPath(), "-version")
data, err := cmd.Output() 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) { func (p *process) Start() (err error) {
if p.IsRunning() { if p.IsRunning() {
return errors.New("xray is already running") return errors.New("xray is already running")
@ -245,6 +272,7 @@ func (p *process) Start() (err error) {
return nil return nil
} }
// Stop terminates the running Xray process.
func (p *process) Stop() error { func (p *process) Stop() error {
if !p.IsRunning() { if !p.IsRunning() {
return errors.New("xray is not running") 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 { func writeCrashReport(m []byte) error {
crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log" crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log"
return os.WriteFile(crashReportPath, m, os.ModePerm) return os.WriteFile(crashReportPath, m, os.ModePerm)

View file

@ -1,5 +1,7 @@
package xray package xray
// Traffic represents network traffic statistics for Xray connections.
// It tracks upload and download bytes for inbound or outbound traffic.
type Traffic struct { type Traffic struct {
IsInbound bool IsInbound bool
IsOutbound bool IsOutbound bool