package web import ( "context" "embed" "github.com/BurntSushi/toml" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" "html/template" "io" "io/fs" "net" "net/http" "os" "runtime" "strconv" "time" "x-ui/config" "x-ui/logger" "x-ui/util/common" "x-ui/web/controller" "x-ui/web/service" ) //go:embed assets/* var assetsFS embed.FS //go:embed html/* var htmlFS embed.FS //go:embed translation/* var i18nFS embed.FS type wrapAssetsFS struct { embed.FS } func (f *wrapAssetsFS) Open(name string) (fs.File, error) { return f.FS.Open("assets/" + name) } func stopServer(s *Server) { s.Stop() } type Server struct { *server } func NewServer() *Server { s := &Server{newServer()} runtime.SetFinalizer(s, stopServer) return s } type server struct { listener net.Listener index *controller.IndexController server *controller.ServerController xui *controller.XUIController xrayService service.XrayService settingService service.SettingService ctx context.Context cancel context.CancelFunc } func newServer() *server { ctx, cancel := context.WithCancel(context.Background()) return &server{ ctx: ctx, cancel: cancel, } } 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() secret, err := s.settingService.GetSecret() if err != nil { return nil, err } basePath, err := s.settingService.GetBasePath() if err != nil { return nil, err } store := cookie.NewStore(secret) engine.Use(sessions.Sessions("session", store)) engine.Use(func(c *gin.Context) { c.Set("base_path", basePath) }) err = s.initI18n(engine) if err != nil { return nil, err } if config.IsDebug() { // for develop engine.LoadHTMLGlob("web/html/**/*.html") engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets"))) } else { t := template.New("") t, err = t.ParseFS(htmlFS, "html/**/*.html") if err != nil { return nil, err } engine.SetHTMLTemplate(t) engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS})) } g := engine.Group(basePath) s.index = controller.NewIndexController(g) s.server = controller.NewServerController(g) s.xui = controller.NewXUIController(g) return engine, nil } func (s *server) initI18n(engine *gin.Engine) error { bundle := i18n.NewBundle(language.SimplifiedChinese) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } data, err := i18nFS.ReadFile(path) if err != nil { return err } _, err = bundle.ParseMessageFileBytes(data, path) return err }) if err != nil { return err } findI18nParamNames := func(key string) []string { names := make([]string, 0) keyLen := len(key) for i := 0; i < keyLen-1; i++ { if key[i:i+2] == "{{" { // 判断开头 "{{" j := i + 2 isFind := false for ; j < keyLen-1; j++ { if key[j:j+2] == "}}" { // 结尾 "}}" isFind = true break } } if isFind { names = append(names, key[i+3:j]) } } } return names } var localizer *i18n.Localizer engine.FuncMap["i18n"] = func(key string, params ...string) (string, error) { names := findI18nParamNames(key) if len(names) != len(params) { return "", common.NewError("find names:", names, "---------- params:", params, "---------- num not equal") } templateData := map[string]interface{}{} for i := range names { templateData[names[i]] = params[i] } return localizer.Localize(&i18n.LocalizeConfig{ MessageID: key, TemplateData: templateData, }) } engine.Use(func(c *gin.Context) { accept := c.GetHeader("Accept-Language") localizer = i18n.NewLocalizer(bundle, accept) c.Set("localizer", localizer) c.Next() }) return nil } func (s *server) startTask() { go func() { err := s.xrayService.StartXray() if err != nil { logger.Warning("start xray failed:", err) } ticker := time.NewTicker(time.Second * 30) defer ticker.Stop() for { select { case <-s.ctx.Done(): return case <-ticker.C: } if s.xrayService.IsXrayRunning() { continue } err := s.xrayService.StartXray() if err != nil { logger.Warning("start xray failed:", err) } } }() } func (s *server) Run() error { engine, err := s.initRouter() if err != nil { return err } s.startTask() 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)) if certFile != "" || keyFile != "" { logger.Info("web server run https on", listenAddr) return engine.RunTLS(listenAddr, certFile, keyFile) } else { logger.Info("web server run http on", listenAddr) return engine.Run(listenAddr) } } func (s *Server) Stop() error { s.cancel() return s.listener.Close() }