Files
trihuy-russian/web/web.go
T

507 lines
13 KiB
Go
Raw Normal View History

2025-09-20 09:35:50 +02:00
// Package web provides the main web server implementation for the 3x-ui panel,
// including HTTP/HTTPS serving, routing, templates, and background job scheduling.
2023-02-09 22:48:06 +03:30
package web
import (
"context"
"crypto/tls"
"embed"
"html/template"
"io"
"io/fs"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
2024-03-11 01:01:24 +03:30
2025-09-19 10:05:43 +02:00
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/util/common"
"github.com/mhsanaei/3x-ui/v2/web/controller"
"github.com/mhsanaei/3x-ui/v2/web/job"
"github.com/mhsanaei/3x-ui/v2/web/locale"
"github.com/mhsanaei/3x-ui/v2/web/middleware"
"github.com/mhsanaei/3x-ui/v2/web/network"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/websocket"
2023-02-09 22:48:06 +03:30
2023-12-05 00:18:16 +03:30
"github.com/gin-contrib/gzip"
2024-04-21 00:56:55 +03:30
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
2023-02-09 22:48:06 +03:30
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
)
2025-09-12 13:04:36 +02:00
//go:embed assets
2023-02-09 22:48:06 +03:30
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
}
2025-09-20 09:35:50 +02:00
// EmbeddedHTML returns the embedded HTML templates filesystem for reuse by other servers.
2025-09-14 20:16:40 +02:00
func EmbeddedHTML() embed.FS {
return htmlFS
}
2025-09-20 09:35:50 +02:00
// EmbeddedAssets returns the embedded assets filesystem for reuse by other servers.
2025-09-14 20:16:40 +02:00
func EmbeddedAssets() embed.FS {
return assetsFS
}
2025-09-20 09:35:50 +02:00
// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs.
2023-02-09 22:48:06 +03:30
type Server struct {
httpServer *http.Server
listener net.Listener
2025-09-24 11:47:14 +02:00
index *controller.IndexController
panel *controller.XUIController
api *controller.APIController
ws *controller.WebSocketController
2023-02-09 22:48:06 +03:30
xrayService service.XrayService
settingService service.SettingService
tgbotService service.Tgbot
customGeoService *service.CustomGeoService
2023-02-09 22:48:06 +03:30
wsHub *websocket.Hub
2023-02-09 22:48:06 +03:30
cron *cron.Cron
ctx context.Context
cancel context.CancelFunc
}
2025-09-20 09:35:50 +02:00
// NewServer creates a new web server instance with a cancellable context.
2023-02-09 22:48:06 +03:30
func NewServer() *Server {
ctx, cancel := context.WithCancel(context.Background())
return &Server{
ctx: ctx,
cancel: cancel,
}
}
2025-09-20 09:35:50 +02:00
// getHtmlFiles walks the local `web/html` directory and returns a list of
// template file paths. Used only in debug/development mode.
2023-02-09 22:48:06 +03:30
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
}
2025-09-20 09:35:50 +02:00
// getHtmlTemplate parses embedded HTML templates from the bundled `htmlFS`
// using the provided template function map and returns the resulting
// template set for production usage.
2023-02-09 22:48:06 +03:30
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
}
2025-09-20 09:35:50 +02:00
// initRouter initializes Gin, registers middleware, templates, static
// assets, controllers and returns the configured engine.
2023-02-09 22:48:06 +03:30
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()
2023-04-09 23:13:18 +03:30
2023-05-31 01:24:18 +04:30
webDomain, err := s.settingService.GetWebDomain()
if err != nil {
return nil, err
}
if webDomain != "" {
engine.Use(middleware.DomainValidatorMiddleware(webDomain))
}
2023-02-09 22:48:06 +03:30
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))
2023-02-09 22:48:06 +03:30
assetsBasePath := basePath + "assets/"
store := cookie.NewStore(secret)
2025-09-12 13:04:36 +02:00
// Configure default session cookie options, including expiration (MaxAge)
sessionOptions := sessions.Options{
Path: basePath,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
2025-09-12 13:04:36 +02:00
}
if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil && sessionMaxAge > 0 {
sessionOptions.MaxAge = sessionMaxAge * 60 // minutes -> seconds
}
store.Options(sessionOptions)
2024-07-05 14:33:04 +02:00
engine.Use(sessions.Sessions("3x-ui", store))
2023-02-09 22:48:06 +03:30
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")
}
})
2023-05-20 19:43:59 +04:30
// init i18n
err = locale.InitLocalizer(i18nFS, &s.settingService)
2023-02-09 22:48:06 +03:30
if err != nil {
return nil, err
}
2023-05-20 19:46:05 +04:30
// Apply locale middleware for i18n
2023-05-21 03:29:27 +04:30
i18nWebFunc := func(key string, params ...string) string {
2023-05-20 19:46:05 +04:30
return locale.I18n(locale.Web, key, params...)
}
2025-09-12 13:04:36 +02:00
// Register template functions before loading templates
funcMap := template.FuncMap{
"i18n": i18nWebFunc,
}
engine.SetFuncMap(funcMap)
2023-05-20 19:46:05 +04:30
engine.Use(locale.LocalizerMiddleware())
2023-05-20 19:43:59 +04:30
// set static files and template
2023-02-09 22:48:06 +03:30
if config.IsDebug() {
2023-05-20 19:43:59 +04:30
// for development
2023-02-09 22:48:06 +03:30
files, err := s.getHtmlFiles()
if err != nil {
return nil, err
}
2025-09-12 13:04:36 +02:00
// Use the registered func map with the loaded templates
2023-02-09 22:48:06 +03:30
engine.LoadHTMLFiles(files...)
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
} else {
2023-05-20 19:43:59 +04:30
// for production
2025-09-12 13:04:36 +02:00
template, err := s.getHtmlTemplate(funcMap)
2023-02-09 22:48:06 +03:30
if err != nil {
return nil, err
}
2023-05-20 19:43:59 +04:30
engine.SetHTMLTemplate(template)
2023-02-09 22:48:06 +03:30
engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
}
2023-05-14 01:42:08 +04:30
// Apply the redirect middleware (`/xui` to `/panel`)
2023-05-31 01:24:18 +04:30
engine.Use(middleware.RedirectMiddleware(basePath))
2023-05-14 01:42:08 +04:30
2023-02-09 22:48:06 +03:30
g := engine.Group(basePath)
s.index = controller.NewIndexController(g)
2023-05-12 22:36:05 +04:30
s.panel = controller.NewXUIController(g)
s.api = controller.NewAPIController(g, s.customGeoService)
2023-02-09 22:48:06 +03:30
// Initialize WebSocket hub
s.wsHub = websocket.NewHub()
go s.wsHub.Run()
// Initialize WebSocket controller
s.ws = controller.NewWebSocketController(s.wsHub)
// Register WebSocket route with basePath (g already has basePath prefix)
g.GET("/ws", s.ws.HandleWebSocket)
2025-10-02 01:47:12 +02:00
// Chrome DevTools endpoint for debugging web apps
engine.GET("/.well-known/appspecific/com.chrome.devtools.json", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
2025-09-24 21:30:58 +02:00
// Add a catch-all route to handle undefined paths and return 404
engine.NoRoute(func(c *gin.Context) {
c.AbortWithStatus(http.StatusNotFound)
})
2023-02-09 22:48:06 +03:30
return engine, nil
}
2025-09-20 09:35:50 +02:00
// startTask schedules background jobs (Xray checks, traffic jobs, cron
// jobs) which the panel relies on for periodic maintenance and monitoring.
2023-02-09 22:48:06 +03:30
func (s *Server) startTask() {
s.customGeoService.EnsureOnStartup()
2023-02-09 22:48:06 +03:30
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())
2023-02-09 22:48:06 +03:30
// Check if xray needs to be restarted every 30 seconds
s.cron.AddFunc("@every 30s", func() {
2023-09-06 18:22:37 +03:30
if s.xrayService.IsNeedRestartAndSetFalse() {
err := s.xrayService.RestartXray(false)
if err != nil {
logger.Error("restart xray failed:", err)
}
}
})
2023-02-09 22:48:06 +03:30
go func() {
time.Sleep(time.Second * 5)
2023-02-18 16:07:32 +03:30
// Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray
2023-02-09 22:48:06 +03:30
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}()
2023-08-01 23:58:16 +03:00
// check client ips from log file every 10 sec
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
2023-02-28 23:24:29 +03:30
2023-08-24 17:14:51 +03:30
// check client ips from log file every day
s.cron.AddJob("@daily", job.NewClearLogsJob())
2023-07-01 15:56:43 +03:30
2025-09-16 13:41:05 +02:00
// Inbound traffic reset jobs
2026-04-19 22:37:34 +03:00
// Run every hour
s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly"))
2025-09-16 13:41:05 +02:00
// Run once a day, midnight
s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("daily"))
// Run once a week, midnight between Sat/Sun
s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly"))
// Run once a month, midnight, first of month
s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly"))
2025-10-02 01:47:12 +02:00
// LDAP sync scheduling
if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled {
runtime, err := s.settingService.GetLdapSyncCron()
if err != nil || runtime == "" {
runtime = "@every 1m"
}
j := job.NewLdapSyncJob()
// job has zero-value services with method receivers that read settings on demand
s.cron.AddJob(runtime, j)
}
2025-09-28 22:00:16 +03:00
2023-02-18 16:07:32 +03:30
// Make a traffic condition every day, 8:30
2023-02-09 22:48:06 +03:30
var entry cron.EntryID
2024-07-07 11:55:59 +02:00
isTgbotenabled, err := s.settingService.GetTgbotEnabled()
2023-02-09 22:48:06 +03:30
if (err == nil) && (isTgbotenabled) {
runtime, err := s.settingService.GetTgbotRuntime()
2026-04-26 17:34:31 +02:00
if err != nil {
logger.Warningf("Add NewStatsNotifyJob: failed to load runtime: %v; using default @daily", err)
runtime = "@daily"
} else if strings.TrimSpace(runtime) == "" {
logger.Warning("Add NewStatsNotifyJob runtime is empty, using default @daily")
2023-02-09 22:48:06 +03:30
runtime = "@daily"
}
logger.Infof("Tg notify enabled,run at %s", runtime)
2023-02-18 16:07:32 +03:30
_, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob())
2023-02-09 22:48:06 +03:30
if err != nil {
2026-04-26 17:34:31 +02:00
logger.Warningf("Add NewStatsNotifyJob: failed to schedule runtime %q: %v", runtime, err)
2023-02-09 22:48:06 +03:30
return
}
2023-03-17 19:37:49 +03:30
// check for Telegram bot callback query hash storage reset
s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob())
2023-03-17 19:37:49 +03:30
// 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())
}
2023-02-09 22:48:06 +03:30
} else {
s.cron.Remove(entry)
}
}
2025-09-20 09:35:50 +02:00
// Start initializes and starts the web server with configured settings, routes, and background jobs.
2023-02-09 22:48:06 +03:30
func (s *Server) Start() (err error) {
2024-03-11 01:01:24 +03:30
// This is an anonymous function, no function name
2023-02-09 22:48:06 +03:30
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()
s.customGeoService = service.NewCustomGeoService()
2023-02-09 22:48:06 +03:30
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)
2024-02-25 19:37:03 +03:30
if err == nil {
c := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener = network.NewAutoHttpsListener(listener)
listener = tls.NewListener(listener, c)
2024-07-08 23:08:00 +02:00
logger.Info("Web server running HTTPS on", listener.Addr())
2024-02-25 19:37:03 +03:30
} else {
2024-07-08 23:08:00 +02:00
logger.Error("Error loading certificates:", err)
logger.Info("Web server running HTTP on", listener.Addr())
2023-02-09 22:48:06 +03:30
}
} else {
2024-07-08 23:08:00 +02:00
logger.Info("Web server running HTTP on", listener.Addr())
2023-02-09 22:48:06 +03:30
}
s.listener = listener
s.httpServer = &http.Server{
Handler: engine,
}
go func() {
s.httpServer.Serve(listener)
}()
s.startTask()
2024-07-07 11:55:59 +02:00
isTgbotenabled, err := s.settingService.GetTgbotEnabled()
2023-03-17 19:37:49 +03:30
if (err == nil) && (isTgbotenabled) {
tgBot := s.tgbotService.NewTgbot()
2023-05-20 20:08:01 +04:30
tgBot.Start(i18nFS)
2023-03-17 19:37:49 +03:30
}
2023-02-09 22:48:06 +03:30
return nil
}
2025-09-20 09:35:50 +02:00
// Stop gracefully shuts down the web server, stops Xray, cron jobs, and Telegram bot.
2023-02-09 22:48:06 +03:30
func (s *Server) Stop() error {
s.cancel()
s.xrayService.StopXray()
if s.cron != nil {
s.cron.Stop()
}
2023-05-20 19:39:01 +04:30
if s.tgbotService.IsRunning() {
2023-03-17 19:37:49 +03:30
s.tgbotService.Stop()
}
// Gracefully stop WebSocket hub
if s.wsHub != nil {
s.wsHub.Stop()
}
2023-02-09 22:48:06 +03:30
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)
}
2025-09-20 09:35:50 +02:00
// GetCtx returns the server's context for cancellation and deadline management.
2023-02-09 22:48:06 +03:30
func (s *Server) GetCtx() context.Context {
return s.ctx
}
2025-09-20 09:35:50 +02:00
// GetCron returns the server's cron scheduler instance.
2023-02-09 22:48:06 +03:30
func (s *Server) GetCron() *cron.Cron {
return s.cron
}
// GetWSHub returns the WebSocket hub instance.
func (s *Server) GetWSHub() any {
return s.wsHub
}
2026-02-20 00:03:16 +01:00
func (s *Server) RestartXray() error {
return s.xrayService.RestartXray(true)
}