// Package main is the entry point for the 3x-ui web panel application. // It initializes the database, web server, and handles command-line operations for managing the panel. package main import ( "encoding/json" "flag" "fmt" "log" "os" "os/signal" "path/filepath" "syscall" _ "unsafe" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/sub" "github.com/mhsanaei/3x-ui/v2/util/crypto" "github.com/mhsanaei/3x-ui/v2/util/sys" "github.com/mhsanaei/3x-ui/v2/web" "github.com/mhsanaei/3x-ui/v2/web/entity" "github.com/mhsanaei/3x-ui/v2/web/global" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/joho/godotenv" "github.com/op/go-logging" ) // runWebServer initializes and starts the web server for the 3x-ui panel. func runWebServer() { log.Printf("Starting %v %v", config.GetName(), config.GetVersion()) switch config.GetLogLevel() { case config.Debug: logger.InitLogger(logging.DEBUG) case config.Info: logger.InitLogger(logging.INFO) case config.Notice: logger.InitLogger(logging.NOTICE) case config.Warning: logger.InitLogger(logging.WARNING) case config.Error: logger.InitLogger(logging.ERROR) default: log.Fatalf("Unknown log level: %v", config.GetLogLevel()) } godotenv.Load() err := database.InitDB() if err != nil { log.Fatalf("Error initializing database: %v", err) } var server *web.Server server = web.NewServer() global.SetWebServer(server) err = server.Start() if err != nil { log.Fatalf("Error starting web server: %v", err) return } var subServer *sub.Server subServer = sub.NewServer() global.SetSubServer(subServer) err = subServer.Start() if err != nil { log.Fatalf("Error starting sub server: %v", err) return } sigCh := make(chan os.Signal, 1) // Trap shutdown signals signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1) for { sig := <-sigCh switch sig { case syscall.SIGHUP: logger.Info("Received SIGHUP signal. Restarting servers...") // --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart --- service.StopBot() // -- err := server.Stop() if err != nil { logger.Debug("Error stopping web server:", err) } err = subServer.Stop() if err != nil { logger.Debug("Error stopping sub server:", err) } server = web.NewServer() global.SetWebServer(server) err = server.Start() if err != nil { log.Fatalf("Error restarting web server: %v", err) return } log.Println("Web server restarted successfully.") subServer = sub.NewServer() global.SetSubServer(subServer) err = subServer.Start() if err != nil { log.Fatalf("Error restarting sub server: %v", err) return } log.Println("Sub server restarted successfully.") case sys.SIGUSR1: logger.Info("Received USR1 signal, restarting xray-core...") err := server.RestartXray() if err != nil { logger.Error("Failed to restart xray-core:", err) } default: // --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown --- service.StopBot() // ------------------------------------------------------------ server.Stop() subServer.Stop() log.Println("Shutting down servers.") return } } } // resetSetting resets all panel settings to their default values. func resetSetting() { err := database.InitDB() if err != nil { fmt.Println("Failed to initialize database:", err) return } settingService := service.SettingService{} err = settingService.ResetSettings() if err != nil { fmt.Println("Failed to reset settings:", err) } else { fmt.Println("Settings successfully reset.") } } // showSetting displays the current panel settings if show is true. func showSetting(show bool) { if show { settingService := service.SettingService{} port, err := settingService.GetPort() if err != nil { fmt.Println("get current port failed, error info:", err) } subPort, err := settingService.GetSubPort() if err != nil { fmt.Println("get current sub port failed, error info:", err) } webBasePath, err := settingService.GetBasePath() if err != nil { fmt.Println("get webBasePath failed, error info:", err) } listen, err := settingService.GetListen() if err != nil { fmt.Println("get current listen failed, error info:", err) } subListen, err := settingService.GetSubListen() if err != nil { fmt.Println("get current sub listen failed, error info:", err) } certFile, err := settingService.GetCertFile() if err != nil { fmt.Println("get cert file failed, error info:", err) } keyFile, err := settingService.GetKeyFile() if err != nil { fmt.Println("get key file failed, error info:", err) } userService := service.UserService{} userModel, err := userService.GetFirstUser() if err != nil { fmt.Println("get current user info failed, error info:", err) } if userModel.Username == "" || userModel.Password == "" { fmt.Println("current username or password is empty") } fmt.Println("current panel settings as follows:") if certFile == "" || keyFile == "" { fmt.Println("Warning: Panel is not secure with SSL") } else { fmt.Println("Panel is secure with SSL") } hasDefaultCredential := func() bool { return userModel.Username == "admin" && crypto.CheckPasswordHash(userModel.Password, "admin") }() fmt.Println("hasDefaultCredential:", hasDefaultCredential) fmt.Println("port:", port) fmt.Println("listen:", listen) fmt.Println("subPort:", subPort) fmt.Println("subListen:", subListen) fmt.Println("webBasePath:", webBasePath) } } // updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter. func updateTgbotEnableSts(status bool) { settingService := service.SettingService{} currentTgSts, err := settingService.GetTgbotEnabled() if err != nil { fmt.Println(err) return } logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status) if currentTgSts != status { err := settingService.SetTgbotEnabled(status) if err != nil { fmt.Println(err) return } else { logger.Infof("SetTgbotEnabled[%v] success", status) } } } // updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule. func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) { err := database.InitDB() if err != nil { fmt.Println("Error initializing database:", err) return } settingService := service.SettingService{} if tgBotToken != "" { err := settingService.SetTgBotToken(tgBotToken) if err != nil { fmt.Printf("Error setting Telegram bot token: %v\n", err) return } logger.Info("Successfully updated Telegram bot token.") } if tgBotRuntime != "" { err := settingService.SetTgbotRuntime(tgBotRuntime) if err != nil { fmt.Printf("Error setting Telegram bot runtime: %v\n", err) return } logger.Infof("Successfully updated Telegram bot runtime to [%s].", tgBotRuntime) } if tgBotChatid != "" { err := settingService.SetTgBotChatId(tgBotChatid) if err != nil { fmt.Printf("Error setting Telegram bot chat ID: %v\n", err) return } logger.Info("Successfully updated Telegram bot chat ID.") } } // updateSetting updates various panel settings including ports, credentials, base path, listen IPs, and two-factor authentication. func updateSetting(port int, subPort int, username string, password string, webBasePath string, listenIP string, subListenIP string, resetTwoFactor bool) { err := database.InitDB() if err != nil { fmt.Println("Database initialization failed:", err) return } settingService := service.SettingService{} userService := service.UserService{} if port > 0 { err := settingService.SetPort(port) if err != nil { fmt.Println("Failed to set port:", err) } else { fmt.Printf("Port set successfully: %v\n", port) } } if username != "" || password != "" { err := userService.UpdateFirstUser(username, password) if err != nil { fmt.Println("Failed to update username and password:", err) } else { fmt.Println("Username and password updated successfully") } } if webBasePath != "" { err := settingService.SetBasePath(webBasePath) if err != nil { fmt.Println("Failed to set base URI path:", err) } else { fmt.Println("Base URI path set successfully") } } if resetTwoFactor { err := settingService.SetTwoFactorEnable(false) if err != nil { fmt.Println("Failed to reset two-factor authentication:", err) } else { settingService.SetTwoFactorToken("") fmt.Println("Two-factor authentication reset successfully") } } if listenIP != "" { err := settingService.SetListen(listenIP) if err != nil { fmt.Println("Failed to set listen IP:", err) } else { fmt.Printf("listen %v set successfully\n", listenIP) } } if subPort > 0 { err := settingService.SetSubPort(subPort) if err != nil { fmt.Println("Failed to set sub port:", err) } else { fmt.Printf("Sub port set successfully: %v\n", subPort) } } if subListenIP != "" { err := settingService.SetSubListen(subListenIP) if err != nil { fmt.Println("Failed to set sub listen IP:", err) } else { fmt.Printf("sub listen %v set successfully\n", subListenIP) } } } // updateCert updates the SSL certificate files for the panel. func updateCert(publicKey string, privateKey string) { err := database.InitDB() if err != nil { fmt.Println(err) return } if (privateKey != "" && publicKey != "") || (privateKey == "" && publicKey == "") { settingService := service.SettingService{} err = settingService.SetCertFile(publicKey) if err != nil { fmt.Println("set certificate public key failed:", err) } else { fmt.Println("set certificate public key success") } err = settingService.SetKeyFile(privateKey) if err != nil { fmt.Println("set certificate private key failed:", err) } else { fmt.Println("set certificate private key success") } err = settingService.SetSubCertFile(publicKey) if err != nil { fmt.Println("set certificate for subscription public key failed:", err) } else { fmt.Println("set certificate for subscription public key success") } err = settingService.SetSubKeyFile(privateKey) if err != nil { fmt.Println("set certificate for subscription private key failed:", err) } else { fmt.Println("set certificate for subscription private key success") } } else { fmt.Println("both public and private key should be entered.") } } // GetCertificate displays the current SSL certificate settings if getCert is true. func GetCertificate(getCert bool) { if getCert { settingService := service.SettingService{} certFile, err := settingService.GetCertFile() if err != nil { fmt.Println("get cert file failed, error info:", err) } keyFile, err := settingService.GetKeyFile() if err != nil { fmt.Println("get key file failed, error info:", err) } fmt.Println("cert:", certFile) fmt.Println("key:", keyFile) } } // GetListenIP displays the current panel listen IP address if getListen is true. func GetListenIP(getListen bool) { if getListen { settingService := service.SettingService{} ListenIP, err := settingService.GetListen() if err != nil { log.Printf("Failed to retrieve listen IP: %v", err) return } fmt.Println("listenIP:", ListenIP) } } // migrateDb performs database migration operations for the 3x-ui panel. func migrateDb() { inboundService := service.InboundService{} err := database.InitDB() if err != nil { log.Fatal(err) } fmt.Println("Start migrating database...") inboundService.MigrateDB() fmt.Println("Migration done!") } func defaultDatabaseSetting() *entity.DatabaseSetting { databaseService := &service.DatabaseService{} setting, err := databaseService.GetSetting() if err == nil && setting != nil { return setting } return entity.DatabaseSettingFromConfig(config.DefaultDatabaseConfig()) } func addDatabaseFlags(fs *flag.FlagSet, setting *entity.DatabaseSetting) { fs.StringVar(&setting.Driver, "driver", setting.Driver, "Database driver: sqlite or postgres") fs.StringVar(&setting.SQLitePath, "sqlite-path", setting.SQLitePath, "SQLite database path") fs.StringVar(&setting.PostgresMode, "postgres-mode", setting.PostgresMode, "PostgreSQL mode: local or external") fs.StringVar(&setting.PostgresHost, "postgres-host", setting.PostgresHost, "PostgreSQL host") fs.IntVar(&setting.PostgresPort, "postgres-port", setting.PostgresPort, "PostgreSQL port") fs.StringVar(&setting.PostgresDBName, "postgres-db", setting.PostgresDBName, "PostgreSQL database name") fs.StringVar(&setting.PostgresUser, "postgres-user", setting.PostgresUser, "PostgreSQL user") fs.StringVar(&setting.PostgresPassword, "postgres-password", "", "PostgreSQL password") fs.StringVar(&setting.PostgresSSLMode, "postgres-sslmode", setting.PostgresSSLMode, "PostgreSQL sslmode") fs.BoolVar(&setting.ManagedLocally, "postgres-local", setting.ManagedLocally, "Treat PostgreSQL as locally managed") } func writeExportFile(outputPath string, defaultName string, data []byte) (string, error) { targetPath := outputPath if targetPath == "" { targetPath = defaultName } else if info, err := os.Stat(targetPath); err == nil && info.IsDir() { targetPath = filepath.Join(targetPath, defaultName) } if err := os.WriteFile(targetPath, data, 0o600); err != nil { return "", err } return targetPath, nil } func handleDatabaseCommand(args []string) { if len(args) == 0 { fmt.Println("Usage:") fmt.Println(" x-ui database show") fmt.Println(" x-ui database test [database flags]") fmt.Println(" x-ui database switch [database flags]") fmt.Println(" x-ui database export -type portable|sqlite [-out path]") fmt.Println(" x-ui database import -file backup.xui-backup") fmt.Println(" x-ui database install-postgres") return } databaseService := service.DatabaseService{} switch args[0] { case "show": setting, err := databaseService.GetSetting() if err != nil { fmt.Println("Failed to load database settings:", err) return } contents, err := json.MarshalIndent(setting, "", " ") if err != nil { fmt.Println("Failed to serialize database settings:", err) return } fmt.Println(string(contents)) case "test": setting := defaultDatabaseSetting() testCmd := flag.NewFlagSet("database test", flag.ExitOnError) addDatabaseFlags(testCmd, setting) _ = testCmd.Parse(args[1:]) if err := databaseService.TestSetting(setting); err != nil { fmt.Println("Database connection test failed:", err) return } fmt.Println("Database connection test succeeded.") case "switch": setting := defaultDatabaseSetting() switchCmd := flag.NewFlagSet("database switch", flag.ExitOnError) addDatabaseFlags(switchCmd, setting) _ = switchCmd.Parse(args[1:]) if err := databaseService.SwitchDatabase(setting); err != nil { fmt.Println("Database switch failed:", err) return } fmt.Println("Database configuration updated. Restart the panel service to apply changes.") case "export": exportCmd := flag.NewFlagSet("database export", flag.ExitOnError) exportType := exportCmd.String("type", "portable", "Export type: portable or sqlite") outputPath := exportCmd.String("out", "", "Output path or directory") _ = exportCmd.Parse(args[1:]) if err := database.InitDB(); err != nil { fmt.Println("Failed to initialize database:", err) return } var ( data []byte filename string err error ) switch *exportType { case "sqlite": data, filename, err = databaseService.ExportNativeSQLite() default: data, filename, err = databaseService.ExportPortableBackup() } if err != nil { fmt.Println("Export failed:", err) return } targetPath, err := writeExportFile(*outputPath, filename, data) if err != nil { fmt.Println("Failed to write backup:", err) return } fmt.Println("Backup exported to:", targetPath) case "import": importCmd := flag.NewFlagSet("database import", flag.ExitOnError) backupFile := importCmd.String("file", "", "Path to .xui-backup or legacy SQLite .db file") _ = importCmd.Parse(args[1:]) if *backupFile == "" { fmt.Println("Import requires -file") return } if err := database.InitDB(); err != nil { fmt.Println("Failed to initialize database:", err) return } file, err := os.Open(*backupFile) if err != nil { fmt.Println("Failed to open backup file:", err) return } defer file.Close() backupType, err := databaseService.ImportBackup(file) if err != nil { fmt.Println("Import failed:", err) return } fmt.Println("Import completed using", backupType, "backup. Restart the panel service to apply changes.") case "install-postgres": output, err := databaseService.InstallLocalPostgres() if err != nil { fmt.Println("PostgreSQL installation failed:", err) return } fmt.Print(output) default: fmt.Println("Unknown database subcommand:", args[0]) } } // main is the entry point of the 3x-ui application. // It parses command-line arguments to run the web server, migrate database, or update settings. func main() { if len(os.Args) < 2 { runWebServer() return } var showVersion bool flag.BoolVar(&showVersion, "v", false, "show version") runCmd := flag.NewFlagSet("run", flag.ExitOnError) settingCmd := flag.NewFlagSet("setting", flag.ExitOnError) var port int var subPort int var username string var password string var webBasePath string var listenIP string var subListenIP string var getListen bool var webCertFile string var webKeyFile string var tgbottoken string var tgbotchatid string var enabletgbot bool var tgbotRuntime string var reset bool var show bool var getCert bool var resetTwoFactor bool settingCmd.BoolVar(&reset, "reset", false, "Reset all settings") settingCmd.BoolVar(&show, "show", false, "Display current settings") settingCmd.IntVar(&port, "port", 0, "Set panel port number") settingCmd.IntVar(&subPort, "subPort", 0, "Set subscription port number") settingCmd.StringVar(&username, "username", "", "Set login username") settingCmd.StringVar(&password, "password", "", "Set login password") settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel") settingCmd.StringVar(&listenIP, "listenIP", "", "set panel listenIP IP") settingCmd.StringVar(&subListenIP, "subListenIP", "", "set subscription listenIP IP") settingCmd.BoolVar(&resetTwoFactor, "resetTwoFactor", false, "Reset two-factor authentication settings") settingCmd.BoolVar(&getListen, "getListen", false, "Display current panel listenIP IP") settingCmd.BoolVar(&getCert, "getCert", false, "Display current certificate settings") settingCmd.StringVar(&webCertFile, "webCert", "", "Set path to public key file for panel") settingCmd.StringVar(&webKeyFile, "webCertKey", "", "Set path to private key file for panel") settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "Set token for Telegram bot") settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "Set cron time for Telegram bot notifications") settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "Set chat ID for Telegram bot notifications") settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "Enable notifications via Telegram bot") oldUsage := flag.Usage flag.Usage = func() { oldUsage() fmt.Println() fmt.Println("Commands:") fmt.Println(" run run web panel") fmt.Println(" migrate migrate form other/old x-ui") fmt.Println(" setting set settings") fmt.Println(" database manage database backend") } flag.Parse() if showVersion { fmt.Println(config.GetVersion()) return } switch os.Args[1] { case "run": err := runCmd.Parse(os.Args[2:]) if err != nil { fmt.Println(err) return } runWebServer() case "migrate": migrateDb() case "setting": err := settingCmd.Parse(os.Args[2:]) if err != nil { fmt.Println(err) return } if reset { resetSetting() } else { updateSetting(port, subPort, username, password, webBasePath, listenIP, subListenIP, resetTwoFactor) } if show { showSetting(show) } if getListen { GetListenIP(getListen) } if getCert { GetCertificate(getCert) } if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") { updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) } if enabletgbot { updateTgbotEnableSts(enabletgbot) } case "cert": err := settingCmd.Parse(os.Args[2:]) if err != nil { fmt.Println(err) return } if reset { updateCert("", "") } else { updateCert(webCertFile, webKeyFile) } case "database": handleDatabaseCommand(os.Args[2:]) default: fmt.Println("Invalid subcommands") fmt.Println() runCmd.Usage() fmt.Println() settingCmd.Usage() } }