mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-08-23 19:36:54 +00:00
![google-labs-jules[bot]](/assets/img/avatar_default.png)
This commit introduces a multi-server architecture to the Sanai panel, allowing you to manage clients across multiple servers from a central panel. Key changes include: - **Database Schema:** Added a `servers` table to store information about slave servers. - **Server Management:** Implemented a new service and controller (`MultiServerService` and `MultiServerController`) for CRUD operations on servers. - **Web UI:** Created a new web page for managing servers, accessible from the sidebar. - **Client Synchronization:** Modified the `InboundService` to synchronize client additions, updates, and deletions across all active slave servers via a REST API. - **API Security:** Added an API key authentication middleware to secure the communication between the master and slave panels. - **Multi-Server Subscriptions:** Updated the subscription service to generate links that include configurations for all active servers. - **Installation Script:** Modified the `install.sh` script to generate a random API key during installation. **Known Issues:** - The integration test for client synchronization (`TestInboundServiceSync`) is currently failing. It seems that the API request to the mock slave server is not being sent correctly or the API key is not being included in the request header. Further investigation is needed to resolve this issue.
489 lines
12 KiB
Go
489 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
_ "unsafe"
|
|
|
|
"x-ui/config"
|
|
"x-ui/database"
|
|
"x-ui/logger"
|
|
"x-ui/sub"
|
|
"x-ui/util/crypto"
|
|
"x-ui/web"
|
|
"x-ui/web/global"
|
|
"x-ui/web/service"
|
|
|
|
"github.com/joho/godotenv"
|
|
"github.com/op/go-logging"
|
|
)
|
|
|
|
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.Warn:
|
|
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(config.GetDBPath())
|
|
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)
|
|
for {
|
|
sig := <-sigCh
|
|
|
|
switch sig {
|
|
case syscall.SIGHUP:
|
|
logger.Info("Received SIGHUP signal. Restarting servers...")
|
|
|
|
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.")
|
|
|
|
default:
|
|
server.Stop()
|
|
subServer.Stop()
|
|
log.Println("Shutting down servers.")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func resetSetting() {
|
|
err := database.InitDB(config.GetDBPath())
|
|
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.")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
webBasePath, err := settingService.GetBasePath()
|
|
if err != nil {
|
|
fmt.Println("get webBasePath 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("webBasePath:", webBasePath)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
|
err := database.InitDB(config.GetDBPath())
|
|
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.")
|
|
}
|
|
}
|
|
|
|
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool, apiKey string) {
|
|
err := database.InitDB(config.GetDBPath())
|
|
if err != nil {
|
|
fmt.Println("Database initialization failed:", err)
|
|
return
|
|
}
|
|
|
|
settingService := service.SettingService{}
|
|
userService := service.UserService{}
|
|
|
|
if apiKey != "" {
|
|
err := settingService.SetAPIKey(apiKey)
|
|
if err != nil {
|
|
fmt.Println("Failed to set API Key:", err)
|
|
} else {
|
|
fmt.Printf("API Key set successfully: %v\n", apiKey)
|
|
}
|
|
}
|
|
|
|
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", listenIP)
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateCert(publicKey string, privateKey string) {
|
|
err := database.InitDB(config.GetDBPath())
|
|
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")
|
|
}
|
|
} else {
|
|
fmt.Println("both public and private key should be entered.")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func migrateDb() {
|
|
inboundService := service.InboundService{}
|
|
|
|
err := database.InitDB(config.GetDBPath())
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("Start migrating database...")
|
|
inboundService.MigrateDB()
|
|
fmt.Println("Migration done!")
|
|
}
|
|
|
|
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 username string
|
|
var password string
|
|
var webBasePath string
|
|
var listenIP 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
|
|
var apiKey string
|
|
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.StringVar(&apiKey, "apiKey", "", "Set API Key")
|
|
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.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")
|
|
}
|
|
|
|
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, username, password, webBasePath, listenIP, resetTwoFactor, apiKey)
|
|
}
|
|
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)
|
|
}
|
|
default:
|
|
fmt.Println("Invalid subcommands")
|
|
fmt.Println()
|
|
runCmd.Usage()
|
|
fmt.Println()
|
|
settingCmd.Usage()
|
|
}
|
|
}
|