package web import ( "context" "crypto/tls" "embed" "html/template" "io" "io/fs" "net" "net/http" "os" "strconv" "strings" "time" "x-ui/config" "x-ui/logger" "x-ui/util/common" "x-ui/web/controller" "x-ui/web/job" "x-ui/web/locale" "x-ui/web/middleware" "x-ui/web/network" "x-ui/web/service" "github.com/gin-contrib/gzip" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/robfig/cron/v3" ) //go:embed assets/* var assetsFS embed.FS //go:embed html/* var htmlFS embed.FS //go:embed translation/* var i18nFS embed.FS var startTime = time.Now() type wrapAssetsFS struct { embed.FS } func (f *wrapAssetsFS) Open(name string) (fs.File, error) { file, err := f.FS.Open("assets/" + name) if err != nil { return nil, err } return &wrapAssetsFile{ File: file, }, nil } type wrapAssetsFile struct { fs.File } func (f *wrapAssetsFile) Stat() (fs.FileInfo, error) { info, err := f.File.Stat() if err != nil { return nil, err } return &wrapAssetsFileInfo{ FileInfo: info, }, nil } type wrapAssetsFileInfo struct { fs.FileInfo } func (f *wrapAssetsFileInfo) ModTime() time.Time { return startTime } type Server struct { httpServer *http.Server listener net.Listener index *controller.IndexController server *controller.ServerController panel *controller.XUIController api *controller.APIController xrayService service.XrayService settingService service.SettingService tgbotService service.Tgbot cron *cron.Cron ctx context.Context cancel context.CancelFunc } func NewServer() *Server { ctx, cancel := context.WithCancel(context.Background()) return &Server{ ctx: ctx, cancel: cancel, } } func (s *Server) getHtmlFiles() ([]string, error) { files := make([]string, 0) dir, _ := os.Getwd() err := fs.WalkDir(os.DirFS(dir), "web/html", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } files = append(files, path) return nil }) if err != nil { return nil, err } return files, nil } func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) { t := template.New("").Funcs(funcMap) err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { newT, err := t.ParseFS(htmlFS, path+"/*.html") if err != nil { // ignore return nil } t = newT } return nil }) if err != nil { return nil, err } return t, nil } func (s *Server) initRouter() (*gin.Engine, error) { if config.IsDebug() { gin.SetMode(gin.DebugMode) } else { gin.DefaultWriter = io.Discard gin.DefaultErrorWriter = io.Discard gin.SetMode(gin.ReleaseMode) } engine := gin.Default() webDomain, err := s.settingService.GetWebDomain() if err != nil { return nil, err } if webDomain != "" { engine.Use(middleware.DomainValidatorMiddleware(webDomain)) } secret, err := s.settingService.GetSecret() if err != nil { return nil, err } basePath, err := s.settingService.GetBasePath() if err != nil { return nil, err } engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/API/"}))) assetsBasePath := basePath + "assets/" store := cookie.NewStore(secret) engine.Use(sessions.Sessions("3x-ui", store)) engine.Use(func(c *gin.Context) { c.Set("base_path", basePath) }) engine.Use(func(c *gin.Context) { uri := c.Request.RequestURI if strings.HasPrefix(uri, assetsBasePath) { c.Header("Cache-Control", "max-age=31536000") } }) // init i18n err = locale.InitLocalizer(i18nFS, &s.settingService) if err != nil { return nil, err } // Apply locale middleware for i18n i18nWebFunc := func(key string, params ...string) string { return locale.I18n(locale.Web, key, params...) } engine.FuncMap["i18n"] = i18nWebFunc engine.Use(locale.LocalizerMiddleware()) // set static files and template if config.IsDebug() { // for development files, err := s.getHtmlFiles() if err != nil { return nil, err } engine.LoadHTMLFiles(files...) engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets"))) } else { // for production template, err := s.getHtmlTemplate(engine.FuncMap) if err != nil { return nil, err } engine.SetHTMLTemplate(template) engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS})) } // Apply the redirect middleware (`/xui` to `/panel`) engine.Use(middleware.RedirectMiddleware(basePath)) g := engine.Group(basePath) s.index = controller.NewIndexController(g) s.server = controller.NewServerController(g) s.panel = controller.NewXUIController(g) s.api = controller.NewAPIController(g) return engine, nil } func (s *Server) startTask() { err := s.xrayService.RestartXray(true) if err != nil { logger.Warning("start xray failed:", err) } // Check whether xray is running every second s.cron.AddJob("@every 1s", job.NewCheckXrayRunningJob()) // Check if xray needs to be restarted every 30 seconds s.cron.AddFunc("@every 30s", func() { if s.xrayService.IsNeedRestartAndSetFalse() { err := s.xrayService.RestartXray(false) if err != nil { logger.Error("restart xray failed:", err) } } }) go func() { time.Sleep(time.Second * 5) // Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray s.cron.AddJob("@every 10s", job.NewXrayTrafficJob()) }() // check client ips from log file every 10 sec s.cron.AddJob("@every 10s", job.NewCheckClientIpJob()) // check client ips from log file every day s.cron.AddJob("@daily", job.NewClearLogsJob()) // Make a traffic condition every day, 8:30 var entry cron.EntryID isTgbotenabled, err := s.settingService.GetTgbotEnabled() if (err == nil) && (isTgbotenabled) { runtime, err := s.settingService.GetTgbotRuntime() if err != nil || runtime == "" { logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime) runtime = "@daily" } logger.Infof("Tg notify enabled,run at %s", runtime) _, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob()) if err != nil { logger.Warning("Add NewStatsNotifyJob error", err) return } // check for Telegram bot callback query hash storage reset s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob()) // Check CPU load and alarm to TgBot if threshold passes cpuThreshold, err := s.settingService.GetTgCpu() if (err == nil) && (cpuThreshold > 0) { s.cron.AddJob("@every 10s", job.NewCheckCpuJob()) } } else { s.cron.Remove(entry) } } func (s *Server) Start() (err error) { // This is an anonymous function, no function name defer func() { if err != nil { s.Stop() } }() loc, err := s.settingService.GetTimeLocation() if err != nil { return err } s.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds()) s.cron.Start() engine, err := s.initRouter() if err != nil { return err } certFile, err := s.settingService.GetCertFile() if err != nil { return err } keyFile, err := s.settingService.GetKeyFile() if err != nil { return err } listen, err := s.settingService.GetListen() if err != nil { return err } port, err := s.settingService.GetPort() if err != nil { return err } listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listener, err := net.Listen("tcp", listenAddr) if err != nil { return err } if certFile != "" || keyFile != "" { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err == nil { c := &tls.Config{ Certificates: []tls.Certificate{cert}, } listener = network.NewAutoHttpsListener(listener) listener = tls.NewListener(listener, c) logger.Info("Web server running HTTPS on", listener.Addr()) } else { logger.Error("Error loading certificates:", err) logger.Info("Web server running HTTP on", listener.Addr()) } } else { logger.Info("Web server running HTTP on", listener.Addr()) } s.listener = listener s.httpServer = &http.Server{ Handler: engine, } go func() { s.httpServer.Serve(listener) }() s.startTask() isTgbotenabled, err := s.settingService.GetTgbotEnabled() if (err == nil) && (isTgbotenabled) { tgBot := s.tgbotService.NewTgbot() tgBot.Start(i18nFS) } return nil } func (s *Server) Stop() error { s.cancel() s.xrayService.StopXray() if s.cron != nil { s.cron.Stop() } if s.tgbotService.IsRunning() { s.tgbotService.Stop() } var err1 error var err2 error if s.httpServer != nil { err1 = s.httpServer.Shutdown(s.ctx) } if s.listener != nil { err2 = s.listener.Close() } return common.Combine(err1, err2) } func (s *Server) GetCtx() context.Context { return s.ctx } func (s *Server) GetCron() *cron.Cron { return s.cron }