main.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. // Package main is the entry point for the 3x-ui web panel application.
  2. // It initializes the database, web server, and handles command-line operations for managing the panel.
  3. package main
  4. import (
  5. "context"
  6. "flag"
  7. "fmt"
  8. "log"
  9. "net/http"
  10. _ "net/http/pprof"
  11. "os"
  12. "os/signal"
  13. "strings"
  14. "syscall"
  15. _ "unsafe"
  16. "github.com/mhsanaei/3x-ui/v3/internal/config"
  17. "github.com/mhsanaei/3x-ui/v3/internal/crypto/nodetoken"
  18. "github.com/mhsanaei/3x-ui/v3/internal/database"
  19. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  20. "github.com/mhsanaei/3x-ui/v3/internal/sub"
  21. "github.com/mhsanaei/3x-ui/v3/internal/tunnelmonitor"
  22. "github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
  23. "github.com/mhsanaei/3x-ui/v3/internal/util/sys"
  24. "github.com/mhsanaei/3x-ui/v3/internal/web"
  25. "github.com/mhsanaei/3x-ui/v3/internal/web/global"
  26. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  27. "github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
  28. "github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot"
  29. "github.com/joho/godotenv"
  30. "github.com/op/go-logging"
  31. )
  32. // cliFallbackTokenName is the single token the CLI regenerates, so `-getApiToken`
  33. // cannot accumulate admin-equivalent credentials that are never revoked.
  34. const cliFallbackTokenName = "cli-fallback"
  35. // installTokenName is minted once on a panel with no tokens and is deliberately
  36. // not the rotated slot, so the credential the installer recorded keeps working.
  37. const installTokenName = "install"
  38. // initNodeTokenCrypto loads the process codec, preferring the key file over
  39. // the environment and failing closed when an enabled policy lacks a key.
  40. func initNodeTokenCrypto() error {
  41. mode, err := nodetoken.ParseMode(config.GetNodeTokenEncryptionMode())
  42. if err != nil {
  43. return err
  44. }
  45. if mode == nodetoken.ModeOff {
  46. c, _ := nodetoken.NewCodec(nodetoken.ModeOff, nil)
  47. nodetoken.Init(c)
  48. return nil
  49. }
  50. ring, ferr := (nodetoken.FileKeySource{Path: config.GetNodeTokenKeyFile()}).Load()
  51. if ferr != nil {
  52. var eerr error
  53. if ring, eerr = (nodetoken.EnvKeySource{Var: config.GetNodeTokenKeyEnv()}).Load(); eerr != nil {
  54. return fmt.Errorf("load node-token key: file: %w; env: %w", ferr, eerr)
  55. }
  56. }
  57. c, err := nodetoken.NewCodec(mode, ring)
  58. if err != nil {
  59. return err
  60. }
  61. nodetoken.Init(c)
  62. // The CLI runs before package logger initialization, so use log.Printf.
  63. log.Printf("node-token encryption enabled (mode=%s, active-key=%s)", config.GetNodeTokenEncryptionMode(), c.ActiveKeyID())
  64. return nil
  65. }
  66. // runWebServer initializes and starts the web server for the 3x-ui panel.
  67. func runWebServer() {
  68. log.Printf("Starting %v %v", config.GetName(), config.GetPanelVersion())
  69. switch config.GetLogLevel() {
  70. case config.Debug:
  71. logger.InitLogger(logging.DEBUG)
  72. case config.Info:
  73. logger.InitLogger(logging.INFO)
  74. case config.Notice:
  75. logger.InitLogger(logging.NOTICE)
  76. case config.Warning:
  77. logger.InitLogger(logging.WARNING)
  78. case config.Error:
  79. logger.InitLogger(logging.ERROR)
  80. default:
  81. log.Fatalf("Unknown log level: %v", config.GetLogLevel())
  82. }
  83. _ = godotenv.Load()
  84. for _, line := range sys.ApplyMemoryTuning() {
  85. logger.Info(line)
  86. }
  87. if os.Getenv("XUI_PPROF") == "true" {
  88. go func() {
  89. logger.Info("pprof profiling server listening on 127.0.0.1:6060")
  90. if err := http.ListenAndServe("127.0.0.1:6060", nil); err != nil {
  91. logger.Warning("pprof server stopped: ", err)
  92. }
  93. }()
  94. }
  95. if err := initNodeTokenCrypto(); err != nil {
  96. log.Fatalf("Error initializing node-token encryption: %v", err)
  97. }
  98. err := database.InitDB(config.GetDBPath())
  99. if err != nil {
  100. log.Fatalf("Error initializing database: %v", err)
  101. }
  102. server := web.NewServer()
  103. global.SetWebServer(server)
  104. err = server.Start()
  105. if err != nil {
  106. log.Fatalf("Error starting web server: %v", err)
  107. }
  108. sub.SetDistFS(web.EmbeddedDist())
  109. service.RegisterSubLinkProvider(sub.NewLinkProvider())
  110. subServer := sub.NewServer()
  111. global.SetSubServer(subServer)
  112. err = subServer.Start()
  113. if err != nil {
  114. log.Fatalf("Error starting sub server: %v", err)
  115. }
  116. sigCh := make(chan os.Signal, 8)
  117. // Trap shutdown signals
  118. signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1, os.Interrupt)
  119. global.SetRestartHook(func() {
  120. select {
  121. case sigCh <- syscall.SIGHUP:
  122. default:
  123. }
  124. })
  125. var stopTunnelHealthMonitor context.CancelFunc
  126. monitorCfg := tunnelmonitor.ConfigFromEnv()
  127. if monitorCfg.Enabled {
  128. if monitorCfg.ProxyURL == "" {
  129. logger.Warning("Tunnel health monitor enabled without XUI_TUNNEL_HEALTH_PROXY: the probe measures host connectivity, not the xray tunnel, so failures will restart xray without fixing host network issues")
  130. }
  131. monitorCtx, cancel := context.WithCancel(context.Background())
  132. stopTunnelHealthMonitor = cancel
  133. monitor, err := tunnelmonitor.New(monitorCfg, func(_ context.Context) error {
  134. logger.Warning("Tunnel health monitor threshold reached, restarting xray-core")
  135. return server.RestartXray()
  136. })
  137. if err != nil {
  138. logger.Warning("Tunnel health monitor disabled: ", err)
  139. } else {
  140. go monitor.Run(monitorCtx)
  141. }
  142. }
  143. for {
  144. sig := <-sigCh
  145. switch sig {
  146. case syscall.SIGHUP:
  147. logger.Info("Received SIGHUP signal. Restarting servers...")
  148. err := server.StopPanelOnly()
  149. if err != nil {
  150. logger.Debug("Error stopping web server:", err)
  151. }
  152. err = subServer.Stop()
  153. if err != nil {
  154. logger.Debug("Error stopping sub server:", err)
  155. }
  156. server = web.NewServer()
  157. global.SetWebServer(server)
  158. err = server.StartPanelOnly()
  159. if err != nil {
  160. log.Fatalf("Error restarting web server: %v", err)
  161. }
  162. log.Println("Web server restarted successfully.")
  163. sub.SetDistFS(web.EmbeddedDist())
  164. subServer = sub.NewServer()
  165. global.SetSubServer(subServer)
  166. err = subServer.Start()
  167. if err != nil {
  168. log.Fatalf("Error restarting sub server: %v", err)
  169. }
  170. log.Println("Sub server restarted successfully.")
  171. case sys.SIGUSR1:
  172. logger.Info("Received USR1 signal, restarting xray-core...")
  173. err := server.RestartXray()
  174. if err != nil {
  175. logger.Error("Failed to restart xray-core:", err)
  176. }
  177. default:
  178. if stopTunnelHealthMonitor != nil {
  179. stopTunnelHealthMonitor()
  180. }
  181. // --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
  182. tgbot.StopBot()
  183. // ------------------------------------------------------------
  184. if err := server.Stop(); err != nil {
  185. logger.Error("Server shutdown failed:", err)
  186. }
  187. _ = subServer.Stop()
  188. log.Println("Shutting down servers.")
  189. return
  190. }
  191. }
  192. }
  193. // resetSetting resets all panel settings to their default values.
  194. func resetSetting() error {
  195. err := database.InitDB(config.GetDBPath())
  196. if err != nil {
  197. fmt.Println("Failed to initialize database:", err)
  198. return err
  199. }
  200. settingService := service.SettingService{}
  201. err = settingService.ResetSettings()
  202. if err != nil {
  203. fmt.Println("Failed to reset settings:", err)
  204. return err
  205. } else {
  206. fmt.Println("Settings successfully reset.")
  207. }
  208. return nil
  209. }
  210. // showSetting displays the current panel settings if show is true.
  211. func showSetting(show bool) {
  212. if show {
  213. settingService := service.SettingService{}
  214. port, err := settingService.GetPort()
  215. if err != nil {
  216. fmt.Println("get current port failed, error info:", err)
  217. }
  218. webBasePath, err := settingService.GetBasePath()
  219. if err != nil {
  220. fmt.Println("get webBasePath failed, error info:", err)
  221. }
  222. certFile, err := settingService.GetCertFile()
  223. if err != nil {
  224. fmt.Println("get cert file failed, error info:", err)
  225. }
  226. keyFile, err := settingService.GetKeyFile()
  227. if err != nil {
  228. fmt.Println("get key file failed, error info:", err)
  229. }
  230. userService := panel.UserService{}
  231. userModel, err := userService.GetFirstUser()
  232. if err != nil {
  233. fmt.Println("get current user info failed, error info:", err)
  234. }
  235. if userModel.Username == "" || userModel.Password == "" {
  236. fmt.Println("current username or password is empty")
  237. }
  238. fmt.Println("current panel settings as follows:")
  239. if certFile == "" || keyFile == "" {
  240. fmt.Println("Warning: Panel is not secure with SSL")
  241. } else {
  242. fmt.Println("Panel is secure with SSL")
  243. }
  244. hasDefaultCredential := func() bool {
  245. return userModel.Username == "admin" && crypto.CheckPasswordHash(userModel.Password, "admin")
  246. }()
  247. fmt.Println("hasDefaultCredential:", hasDefaultCredential)
  248. fmt.Println("port:", port)
  249. fmt.Println("webBasePath:", webBasePath)
  250. }
  251. }
  252. // updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter.
  253. func updateTgbotEnableSts(status bool) {
  254. settingService := service.SettingService{}
  255. currentTgSts, err := settingService.GetTgbotEnabled()
  256. if err != nil {
  257. fmt.Println(err)
  258. return
  259. }
  260. logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status)
  261. if currentTgSts != status {
  262. err := settingService.SetTgbotEnabled(status)
  263. if err != nil {
  264. fmt.Println(err)
  265. return
  266. } else {
  267. logger.Infof("SetTgbotEnabled[%v] success", status)
  268. }
  269. }
  270. }
  271. // updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.
  272. func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
  273. err := database.InitDB(config.GetDBPath())
  274. if err != nil {
  275. fmt.Println("Error initializing database:", err)
  276. return
  277. }
  278. settingService := service.SettingService{}
  279. if tgBotToken != "" {
  280. err := settingService.SetTgBotToken(tgBotToken)
  281. if err != nil {
  282. fmt.Printf("Error setting Telegram bot token: %v\n", err)
  283. return
  284. }
  285. logger.Info("Successfully updated Telegram bot token.")
  286. }
  287. if tgBotRuntime != "" {
  288. err := settingService.SetTgbotRuntime(tgBotRuntime)
  289. if err != nil {
  290. fmt.Printf("Error setting Telegram bot runtime: %v\n", err)
  291. return
  292. }
  293. logger.Infof("Successfully updated Telegram bot runtime to [%s].", tgBotRuntime)
  294. }
  295. if tgBotChatid != "" {
  296. err := settingService.SetTgBotChatId(tgBotChatid)
  297. if err != nil {
  298. fmt.Printf("Error setting Telegram bot chat ID: %v\n", err)
  299. return
  300. }
  301. logger.Info("Successfully updated Telegram bot chat ID.")
  302. }
  303. }
  304. // encryptNodeTokens re-encrypts stored tokens after enablement or rotation.
  305. // It requires migration|required mode and a configured key.
  306. func encryptNodeTokens() {
  307. _ = godotenv.Load()
  308. if err := initNodeTokenCrypto(); err != nil {
  309. fmt.Println("node-token encryption init failed:", err)
  310. os.Exit(1)
  311. }
  312. if err := database.InitDB(config.GetDBPath()); err != nil {
  313. fmt.Println("database initialization failed:", err)
  314. os.Exit(1)
  315. }
  316. changed, skipped, err := (&service.NodeService{}).MigrateNodeTokensToActiveKey()
  317. if err != nil {
  318. fmt.Println("token migration failed:", err)
  319. os.Exit(1)
  320. }
  321. fmt.Printf("node-token migration complete: %d re-encrypted, %d already current/skipped\n", changed, skipped)
  322. }
  323. // updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
  324. func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) error {
  325. err := database.InitDB(config.GetDBPath())
  326. if err != nil {
  327. fmt.Println("Database initialization failed:", err)
  328. return err
  329. }
  330. settingService := service.SettingService{}
  331. userService := panel.UserService{}
  332. if port > 0 {
  333. err := settingService.SetPort(port)
  334. if err != nil {
  335. fmt.Println("Failed to set port:", err)
  336. } else {
  337. fmt.Printf("Port set successfully: %v\n", port)
  338. }
  339. }
  340. if username != "" || password != "" {
  341. err := userService.UpdateFirstUser(username, password)
  342. if err != nil {
  343. fmt.Println("Failed to update username and password:", err)
  344. } else {
  345. fmt.Println("Username and password updated successfully")
  346. }
  347. }
  348. if webBasePath != "" {
  349. err := settingService.SetBasePath(webBasePath)
  350. if err != nil {
  351. fmt.Println("Failed to set base URI path:", err)
  352. } else {
  353. fmt.Println("Base URI path set successfully")
  354. }
  355. }
  356. if resetTwoFactor {
  357. err := settingService.SetTwoFactorEnable(false)
  358. if err != nil {
  359. fmt.Println("Failed to reset two-factor authentication:", err)
  360. } else {
  361. _ = settingService.SetTwoFactorToken("")
  362. fmt.Println("Two-factor authentication reset successfully")
  363. }
  364. }
  365. if listenIP != "" {
  366. err := settingService.SetListen(listenIP)
  367. if err != nil {
  368. fmt.Println("Failed to set listen IP:", err)
  369. } else {
  370. fmt.Printf("listen %v set successfully\n", listenIP)
  371. }
  372. }
  373. return nil
  374. }
  375. // updateCert updates the SSL certificate files for the panel.
  376. func updateCert(publicKey string, privateKey string) {
  377. err := database.InitDB(config.GetDBPath())
  378. if err != nil {
  379. fmt.Println(err)
  380. return
  381. }
  382. if (privateKey != "" && publicKey != "") || (privateKey == "" && publicKey == "") {
  383. settingService := service.SettingService{}
  384. err = settingService.SetCertFile(publicKey)
  385. if err != nil {
  386. fmt.Println("set certificate public key failed:", err)
  387. } else {
  388. fmt.Println("set certificate public key success")
  389. }
  390. err = settingService.SetKeyFile(privateKey)
  391. if err != nil {
  392. fmt.Println("set certificate private key failed:", err)
  393. } else {
  394. fmt.Println("set certificate private key success")
  395. }
  396. err = settingService.SetSubCertFile(publicKey)
  397. if err != nil {
  398. fmt.Println("set certificate for subscription public key failed:", err)
  399. } else {
  400. fmt.Println("set certificate for subscription public key success")
  401. }
  402. err = settingService.SetSubKeyFile(privateKey)
  403. if err != nil {
  404. fmt.Println("set certificate for subscription private key failed:", err)
  405. } else {
  406. fmt.Println("set certificate for subscription private key success")
  407. }
  408. } else {
  409. fmt.Println("both public and private key should be entered.")
  410. }
  411. }
  412. // GetCertificate displays the current SSL certificate settings if getCert is true.
  413. func GetCertificate(getCert bool) {
  414. if getCert {
  415. settingService := service.SettingService{}
  416. certFile, err := settingService.GetCertFile()
  417. if err != nil {
  418. fmt.Println("get cert file failed, error info:", err)
  419. }
  420. keyFile, err := settingService.GetKeyFile()
  421. if err != nil {
  422. fmt.Println("get key file failed, error info:", err)
  423. }
  424. fmt.Println("cert:", certFile)
  425. fmt.Println("key:", keyFile)
  426. }
  427. }
  428. // GetListenIP displays the current panel listen IP address if getListen is true.
  429. func GetListenIP(getListen bool) {
  430. if getListen {
  431. settingService := service.SettingService{}
  432. ListenIP, err := settingService.GetListen()
  433. if err != nil {
  434. log.Printf("Failed to retrieve listen IP: %v", err)
  435. return
  436. }
  437. fmt.Println("listenIP:", ListenIP)
  438. }
  439. }
  440. func GetApiToken(getApiToken bool, tokenName, tokenScope string) {
  441. if !getApiToken {
  442. return
  443. }
  444. // An explicit name applies to both branches below; without one each keeps
  445. // the name it already used, so every existing invocation is unaffected.
  446. name := strings.TrimSpace(tokenName)
  447. err := database.InitDB(config.GetDBPath())
  448. if err != nil {
  449. fmt.Println("open database failed, error info:", err)
  450. return
  451. }
  452. apiTokenService := panel.ApiTokenService{}
  453. tokens, err := apiTokenService.List()
  454. if err != nil {
  455. fmt.Println("get apiToken failed, error info:", err)
  456. return
  457. }
  458. if len(tokens) > 0 {
  459. fmt.Printf("There are %d API token(s) configured. Existing tokens cannot be retrieved in plaintext because only hashes are stored.\n", len(tokens))
  460. fmt.Println("If you have lost your token, you can manage and generate new tokens through the Panel UI (Settings -> API Tokens).")
  461. // Rotate one token per name so repeated calls cannot pile up
  462. // indefinitely many admin-equivalent tokens that never expire.
  463. rotated := name
  464. if rotated == "" {
  465. rotated = cliFallbackTokenName
  466. }
  467. created, err := apiTokenService.RecreateByName(rotated, tokenScope)
  468. if err != nil {
  469. fmt.Println("Failed to create a fallback API token:", err)
  470. return
  471. }
  472. fmt.Printf("\nThe API token %q has been regenerated (any previous one is now invalid):\n", rotated)
  473. fmt.Println("apiToken:", created.Token)
  474. return
  475. }
  476. if name == "" {
  477. name = installTokenName
  478. }
  479. created, err := apiTokenService.Create(name, tokenScope, 0)
  480. if err != nil {
  481. fmt.Println("create apiToken failed, error info:", err)
  482. return
  483. }
  484. fmt.Println("apiToken:", created.Token)
  485. }
  486. // migrateDb performs database migration operations for the 3x-ui panel.
  487. func migrateDb() {
  488. inboundService := service.InboundService{}
  489. logger.InitLogger(logging.INFO)
  490. err := database.InitDB(config.GetDBPath())
  491. if err != nil {
  492. log.Fatal(err)
  493. }
  494. fmt.Println("Start migrating database...")
  495. inboundService.MigrateDB()
  496. fmt.Println("Migration done!")
  497. }
  498. // loadServiceEnvFile loads the systemd EnvironmentFile so CLI subcommands like
  499. // "x-ui setting" hit the same database backend as the panel. godotenv.Load does
  500. // not override variables already in the environment, so it is a no-op for the
  501. // systemd-managed service.
  502. func loadServiceEnvFile() {
  503. for _, path := range config.GetEnvFilePaths() {
  504. if _, err := os.Stat(path); err != nil {
  505. continue
  506. }
  507. if err := godotenv.Load(path); err != nil {
  508. log.Printf("warning: failed to load env file %s: %v", path, err)
  509. }
  510. return
  511. }
  512. }
  513. // main is the entry point of the 3x-ui application.
  514. // It parses command-line arguments to run the web server, migrate database, or update settings.
  515. func main() {
  516. loadServiceEnvFile()
  517. if len(os.Args) < 2 {
  518. runWebServer()
  519. return
  520. }
  521. var showVersion bool
  522. flag.BoolVar(&showVersion, "v", false, "show version")
  523. runCmd := flag.NewFlagSet("run", flag.ExitOnError)
  524. migrateDbCmd := flag.NewFlagSet("migrate-db", flag.ExitOnError)
  525. var migrateDsn string
  526. var migrateSrc string
  527. var migrateDump string
  528. var migrateRestore string
  529. var migrateOut string
  530. migrateDbCmd.StringVar(&migrateDsn, "dsn", "", "Destination PostgreSQL DSN (postgres://user:pass@host:port/db?sslmode=disable)")
  531. migrateDbCmd.StringVar(&migrateSrc, "src", "", "Source SQLite file (defaults to the configured x-ui.db)")
  532. migrateDbCmd.StringVar(&migrateDump, "dump", "", "Write a portable SQL text dump of --src to this file (.db -> .dump)")
  533. migrateDbCmd.StringVar(&migrateRestore, "restore", "", "Rebuild a SQLite database from this SQL text dump (.dump -> .db); requires --out")
  534. migrateDbCmd.StringVar(&migrateOut, "out", "", "Destination SQLite file for --restore (must not already exist)")
  535. settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
  536. var port int
  537. var username string
  538. var password string
  539. var webBasePath string
  540. var listenIP string
  541. var getListen bool
  542. var webCertFile string
  543. var webKeyFile string
  544. var tgbottoken string
  545. var tgbotchatid string
  546. var enabletgbot bool
  547. var tgbotRuntime string
  548. var reset bool
  549. var show bool
  550. var getCert bool
  551. var getApiToken bool
  552. var tokenName string
  553. var tokenScope string
  554. var resetTwoFactor bool
  555. settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
  556. settingCmd.BoolVar(&show, "show", false, "Display current settings")
  557. settingCmd.IntVar(&port, "port", 0, "Set panel port number")
  558. settingCmd.StringVar(&username, "username", "", "Set login username")
  559. settingCmd.StringVar(&password, "password", "", "Set login password")
  560. settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
  561. settingCmd.StringVar(&listenIP, "listenIP", "", "set panel listenIP IP")
  562. settingCmd.BoolVar(&resetTwoFactor, "resetTwoFactor", false, "Reset two-factor authentication settings")
  563. settingCmd.BoolVar(&getListen, "getListen", false, "Display current panel listenIP IP")
  564. settingCmd.BoolVar(&getCert, "getCert", false, "Display current certificate settings")
  565. settingCmd.BoolVar(&getApiToken, "getApiToken", false, "Print an API token for CLI use, regenerating it and invalidating the previous one; on a panel with no tokens yet it mints one instead")
  566. settingCmd.StringVar(&tokenName, "tokenName", "", "Name of the token -getApiToken acts on (default: "+cliFallbackTokenName+", or "+installTokenName+" on a panel with no tokens)")
  567. settingCmd.StringVar(&tokenScope, "tokenScope", "", "Scope of the token -getApiToken issues: admin, monitor or node-sync (default: the scope of the token it replaces, or admin for a new one)")
  568. settingCmd.StringVar(&webCertFile, "webCert", "", "Set path to public key file for panel")
  569. settingCmd.StringVar(&webKeyFile, "webCertKey", "", "Set path to private key file for panel")
  570. settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "Set token for Telegram bot")
  571. settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "Set cron time for Telegram bot notifications")
  572. settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "Set chat ID for Telegram bot notifications")
  573. settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "Enable notifications via Telegram bot")
  574. oldUsage := flag.Usage
  575. flag.Usage = func() {
  576. oldUsage()
  577. fmt.Print(commandHelp())
  578. }
  579. flag.Parse()
  580. if showVersion {
  581. fmt.Println(config.GetPanelVersion())
  582. return
  583. }
  584. switch os.Args[1] {
  585. case "run":
  586. err := runCmd.Parse(os.Args[2:])
  587. if err != nil {
  588. fmt.Println(err)
  589. return
  590. }
  591. runWebServer()
  592. case "migrate":
  593. migrateDb()
  594. case "encrypt-tokens":
  595. encryptNodeTokens()
  596. case "migrate-db":
  597. if err := migrateDbCmd.Parse(os.Args[2:]); err != nil {
  598. fmt.Println(err)
  599. return
  600. }
  601. src := migrateSrc
  602. if src == "" {
  603. src = config.GetDBPath()
  604. }
  605. switch {
  606. case migrateDump != "":
  607. if err := database.DumpSQLite(src, migrateDump); err != nil {
  608. fmt.Println("dump failed:", err)
  609. os.Exit(1)
  610. }
  611. fmt.Printf("Dumped %s -> %s\n", src, migrateDump)
  612. case migrateRestore != "":
  613. if migrateOut == "" {
  614. fmt.Println("--out is required when using --restore: the destination .db path (must not exist)")
  615. return
  616. }
  617. if err := database.RestoreSQLite(migrateRestore, migrateOut); err != nil {
  618. fmt.Println("restore failed:", err)
  619. os.Exit(1)
  620. }
  621. fmt.Printf("Restored %s -> %s\n", migrateRestore, migrateOut)
  622. case migrateDsn != "":
  623. if err := database.MigrateData(src, migrateDsn); err != nil {
  624. fmt.Println("migration failed:", err)
  625. os.Exit(1)
  626. }
  627. default:
  628. fmt.Println("nothing to do: pass --dump <file>, --restore <file> --out <db>, or --dsn <postgres-dsn>")
  629. }
  630. case "setting":
  631. err := settingCmd.Parse(os.Args[2:])
  632. if err != nil {
  633. fmt.Println(err)
  634. return
  635. }
  636. // flag stops parsing at the first non-flag argument, so the `-getApiToken true`
  637. // form drops every flag written after it. Say so instead of acting on a default.
  638. if rest := settingCmd.Args(); len(rest) > 0 {
  639. fmt.Printf("warning: ignored %q and any flags after it; put flags before positional arguments\n", strings.Join(rest, " "))
  640. }
  641. if reset {
  642. if err = resetSetting(); err != nil {
  643. return
  644. }
  645. } else {
  646. if err = updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor); err != nil {
  647. return
  648. }
  649. }
  650. if webCertFile != "" || webKeyFile != "" {
  651. updateCert(webCertFile, webKeyFile)
  652. }
  653. if show {
  654. showSetting(show)
  655. }
  656. if getListen {
  657. GetListenIP(getListen)
  658. }
  659. if getCert {
  660. GetCertificate(getCert)
  661. }
  662. if getApiToken {
  663. GetApiToken(getApiToken, tokenName, tokenScope)
  664. }
  665. if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
  666. updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
  667. }
  668. if enabletgbot {
  669. updateTgbotEnableSts(enabletgbot)
  670. }
  671. case "cert":
  672. err := settingCmd.Parse(os.Args[2:])
  673. if err != nil {
  674. fmt.Println(err)
  675. return
  676. }
  677. if reset {
  678. updateCert("", "")
  679. } else {
  680. updateCert(webCertFile, webKeyFile)
  681. }
  682. default:
  683. fmt.Println("Invalid subcommands")
  684. fmt.Println()
  685. runCmd.Usage()
  686. fmt.Println()
  687. settingCmd.Usage()
  688. }
  689. }
  690. func commandHelp() string {
  691. return `
  692. Commands:
  693. run run web panel
  694. migrate migrate from other/old x-ui
  695. migrate-db SQLite <-> .dump (--dump/--restore) or copy into PostgreSQL (--dsn)
  696. encrypt-tokens encrypt node bearer tokens with the configured active key
  697. setting set settings
  698. `
  699. }