2025-09-20 07:35:50 +00: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 19:18:06 +00:00
package web
import (
"context"
"crypto/tls"
"embed"
"html/template"
"io"
"io/fs"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
2024-03-10 21:31:24 +00:00
2025-09-19 08:05:43 +00: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"
2026-01-03 04:26:00 +00:00
"github.com/mhsanaei/3x-ui/v2/web/websocket"
2023-02-09 19:18:06 +00:00
2023-12-04 20:48:16 +00:00
"github.com/gin-contrib/gzip"
2024-04-20 21:26:55 +00:00
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
2023-02-09 19:18:06 +00:00
"github.com/gin-gonic/gin"
2026-01-11 21:57:04 +00:00
"github.com/mhsanaei/3x-ui/v2/web/cache"
2023-02-09 19:18:06 +00:00
"github.com/robfig/cron/v3"
)
2025-09-12 11:04:36 +00:00
//go:embed assets
2023-02-09 19:18:06 +00:00
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 07:35:50 +00:00
// EmbeddedHTML returns the embedded HTML templates filesystem for reuse by other servers.
2025-09-14 18:16:40 +00:00
func EmbeddedHTML ( ) embed . FS {
return htmlFS
}
2025-09-20 07:35:50 +00:00
// EmbeddedAssets returns the embedded assets filesystem for reuse by other servers.
2025-09-14 18:16:40 +00:00
func EmbeddedAssets ( ) embed . FS {
return assetsFS
}
2025-09-20 07:35:50 +00:00
// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs.
2023-02-09 19:18:06 +00:00
type Server struct {
httpServer * http . Server
listener net . Listener
2025-09-24 09:47:14 +00:00
index * controller . IndexController
panel * controller . XUIController
api * controller . APIController
2026-01-03 04:26:00 +00:00
ws * controller . WebSocketController
2023-02-09 19:18:06 +00:00
xrayService service . XrayService
settingService service . SettingService
2023-03-17 16:07:49 +00:00
tgbotService service . Tgbot
2023-02-09 19:18:06 +00:00
2026-01-03 04:26:00 +00:00
wsHub * websocket . Hub
2023-02-09 19:18:06 +00:00
cron * cron . Cron
ctx context . Context
cancel context . CancelFunc
}
2025-09-20 07:35:50 +00:00
// NewServer creates a new web server instance with a cancellable context.
2023-02-09 19:18:06 +00:00
func NewServer ( ) * Server {
ctx , cancel := context . WithCancel ( context . Background ( ) )
return & Server {
ctx : ctx ,
cancel : cancel ,
}
}
2025-09-20 07:35:50 +00: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 19:18:06 +00:00
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 07:35:50 +00: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 19:18:06 +00:00
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 07:35:50 +00:00
// initRouter initializes Gin, registers middleware, templates, static
// assets, controllers and returns the configured engine.
2023-02-09 19:18:06 +00:00
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 19:43:18 +00:00
2023-05-30 20:54:18 +00:00
webDomain , err := s . settingService . GetWebDomain ( )
if err != nil {
return nil , err
}
if webDomain != "" {
engine . Use ( middleware . DomainValidatorMiddleware ( webDomain ) )
}
2023-02-09 19:18:06 +00:00
secret , err := s . settingService . GetSecret ( )
if err != nil {
return nil , err
}
basePath , err := s . settingService . GetBasePath ( )
if err != nil {
return nil , err
}
2025-09-08 23:22:43 +00:00
engine . Use ( gzip . Gzip ( gzip . DefaultCompression , gzip . WithExcludedPaths ( [ ] string { basePath + "panel/api/" } ) ) )
2023-02-09 19:18:06 +00:00
assetsBasePath := basePath + "assets/"
2026-01-11 21:57:04 +00:00
// Use Redis store for sessions if available, otherwise fallback to cookie store
var store sessions . Store
redisClient := cache . GetClient ( )
if redisClient != nil {
// Use Redis store
store = cache . NewRedisStore ( redisClient , [ ] byte ( secret ) )
logger . Info ( "Using Redis store for sessions" )
} else {
// Fallback to cookie store
store = cookie . NewStore ( secret )
logger . Info ( "Using cookie store for sessions (Redis not available)" )
}
2025-09-12 11:04:36 +00:00
// Configure default session cookie options, including expiration (MaxAge)
if sessionMaxAge , err := s . settingService . GetSessionMaxAge ( ) ; err == nil {
store . Options ( sessions . Options {
Path : "/" ,
MaxAge : sessionMaxAge * 60 , // minutes -> seconds
HttpOnly : true ,
SameSite : http . SameSiteLaxMode ,
} )
}
2024-07-05 12:33:04 +00:00
engine . Use ( sessions . Sessions ( "3x-ui" , store ) )
2023-02-09 19:18:06 +00:00
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 ) {
2026-01-11 21:57:04 +00:00
// Cache static assets for 1 year with immutable flag
c . Header ( "Cache-Control" , "max-age=31536000, public, immutable" )
} else if strings . HasPrefix ( uri , basePath + "panel/api/" ) && c . Request . Method == "GET" {
// For API GET requests, use no-cache but allow conditional requests
// This enables browser caching with validation
c . Header ( "Cache-Control" , "no-cache, must-revalidate" )
2023-02-09 19:18:06 +00:00
}
} )
2023-05-20 15:13:59 +00:00
// init i18n
err = locale . InitLocalizer ( i18nFS , & s . settingService )
2023-02-09 19:18:06 +00:00
if err != nil {
return nil , err
}
2023-05-20 15:16:05 +00:00
// Apply locale middleware for i18n
2023-05-20 22:59:27 +00:00
i18nWebFunc := func ( key string , params ... string ) string {
2023-05-20 15:16:05 +00:00
return locale . I18n ( locale . Web , key , params ... )
}
2025-09-12 11:04:36 +00:00
// Register template functions before loading templates
funcMap := template . FuncMap {
"i18n" : i18nWebFunc ,
}
engine . SetFuncMap ( funcMap )
2023-05-20 15:16:05 +00:00
engine . Use ( locale . LocalizerMiddleware ( ) )
2023-05-20 15:13:59 +00:00
// set static files and template
2023-02-09 19:18:06 +00:00
if config . IsDebug ( ) {
2023-05-20 15:13:59 +00:00
// for development
2023-02-09 19:18:06 +00:00
files , err := s . getHtmlFiles ( )
if err != nil {
return nil , err
}
2025-09-12 11:04:36 +00:00
// Use the registered func map with the loaded templates
2023-02-09 19:18:06 +00:00
engine . LoadHTMLFiles ( files ... )
engine . StaticFS ( basePath + "assets" , http . FS ( os . DirFS ( "web/assets" ) ) )
} else {
2023-05-20 15:13:59 +00:00
// for production
2025-09-12 11:04:36 +00:00
template , err := s . getHtmlTemplate ( funcMap )
2023-02-09 19:18:06 +00:00
if err != nil {
return nil , err
}
2023-05-20 15:13:59 +00:00
engine . SetHTMLTemplate ( template )
2023-02-09 19:18:06 +00:00
engine . StaticFS ( basePath + "assets" , http . FS ( & wrapAssetsFS { FS : assetsFS } ) )
}
2023-05-13 21:12:08 +00:00
// Apply the redirect middleware (`/xui` to `/panel`)
2023-05-30 20:54:18 +00:00
engine . Use ( middleware . RedirectMiddleware ( basePath ) )
2023-05-13 21:12:08 +00:00
2023-02-09 19:18:06 +00:00
g := engine . Group ( basePath )
s . index = controller . NewIndexController ( g )
2023-05-12 18:06:05 +00:00
s . panel = controller . NewXUIController ( g )
2023-02-09 19:18:06 +00:00
s . api = controller . NewAPIController ( g )
2026-01-03 04:26:00 +00:00
// 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-01 23:47:12 +00: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 19:30:58 +00: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 19:18:06 +00:00
return engine , nil
}
2025-09-20 07:35:50 +00:00
// startTask schedules background jobs (Xray checks, traffic jobs, cron
// jobs) which the panel relies on for periodic maintenance and monitoring.
2023-02-09 19:18:06 +00:00
func ( s * Server ) startTask ( ) {
err := s . xrayService . RestartXray ( true )
if err != nil {
logger . Warning ( "start xray failed:" , err )
}
2024-01-01 15:07:56 +00:00
// Check whether xray is running every second
s . cron . AddJob ( "@every 1s" , job . NewCheckXrayRunningJob ( ) )
2023-02-09 19:18:06 +00:00
2024-01-01 15:07:56 +00:00
// Check if xray needs to be restarted every 30 seconds
s . cron . AddFunc ( "@every 30s" , func ( ) {
2023-09-06 14:52:37 +00:00
if s . xrayService . IsNeedRestartAndSetFalse ( ) {
err := s . xrayService . RestartXray ( false )
if err != nil {
logger . Error ( "restart xray failed:" , err )
}
}
} )
2023-02-09 19:18:06 +00:00
go func ( ) {
time . Sleep ( time . Second * 5 )
2026-01-11 21:12:14 +00:00
// Statistics every 3 seconds for faster traffic limit enforcement, start the delay for 5 seconds for the first time, and staggered with the time to restart xray
s . cron . AddJob ( "@every 3s" , job . NewXrayTrafficJob ( ) )
2023-02-09 19:18:06 +00:00
} ( )
2023-08-01 20:58:16 +00:00
// check client ips from log file every 10 sec
s . cron . AddJob ( "@every 10s" , job . NewCheckClientIpJob ( ) )
2026-01-09 12:36:14 +00:00
// Check client HWIDs from log file every 30 seconds
s . cron . AddJob ( "@every 30s" , job . NewCheckClientHWIDJob ( ) )
2023-02-28 19:54:29 +00:00
2023-08-24 13:44:51 +00:00
// check client ips from log file every day
s . cron . AddJob ( "@daily" , job . NewClearLogsJob ( ) )
2023-07-01 12:26:43 +00:00
2025-09-16 11:41:05 +00:00
// Inbound traffic reset jobs
// 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-09-16 07:24:32 +00:00
2025-10-01 23:47:12 +00: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 19:00:16 +00:00
2026-01-05 23:27:12 +00:00
// Node health check job (every 10 seconds)
s . cron . AddJob ( "@every 10s" , job . NewCheckNodeHealthJob ( ) )
// Collect node statistics (traffic and online clients) every 30 seconds
s . cron . AddJob ( "@every 30s" , job . NewCollectNodeStatsJob ( ) )
2026-01-12 02:01:31 +00:00
// Collect node logs and add to panel log buffer every 30 seconds
// Disabled: logs are now pushed from nodes in real-time via push-logs endpoint
// s.cron.AddJob("@every 30s", job.NewCollectNodeLogsJob())
2026-01-05 21:12:53 +00:00
2023-02-18 12:37:32 +00:00
// Make a traffic condition every day, 8:30
2023-02-09 19:18:06 +00:00
var entry cron . EntryID
2024-07-07 09:55:59 +00:00
isTgbotenabled , err := s . settingService . GetTgbotEnabled ( )
2023-02-09 19:18:06 +00:00
if ( err == nil ) && ( isTgbotenabled ) {
runtime , err := s . settingService . GetTgbotRuntime ( )
if err != nil || runtime == "" {
2023-05-20 15:13:59 +00:00
logger . Errorf ( "Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default" , err , runtime )
2023-02-09 19:18:06 +00:00
runtime = "@daily"
}
logger . Infof ( "Tg notify enabled,run at %s" , runtime )
2023-02-18 12:37:32 +00:00
_ , err = s . cron . AddJob ( runtime , job . NewStatsNotifyJob ( ) )
2023-02-09 19:18:06 +00:00
if err != nil {
logger . Warning ( "Add NewStatsNotifyJob error" , err )
return
}
2023-03-17 16:07:49 +00:00
2023-05-20 15:59:28 +00:00
// check for Telegram bot callback query hash storage reset
s . cron . AddJob ( "@every 2m" , job . NewCheckHashStorageJob ( ) )
2023-03-17 16:07:49 +00:00
// 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 19:18:06 +00:00
} else {
s . cron . Remove ( entry )
}
}
2025-09-20 07:35:50 +00:00
// Start initializes and starts the web server with configured settings, routes, and background jobs.
2023-02-09 19:18:06 +00:00
func ( s * Server ) Start ( ) ( err error ) {
2024-03-10 21:31:24 +00:00
// This is an anonymous function, no function name
2023-02-09 19:18:06 +00:00
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 )
2024-02-25 16:07:03 +00:00
if err == nil {
c := & tls . Config {
Certificates : [ ] tls . Certificate { cert } ,
}
listener = network . NewAutoHttpsListener ( listener )
listener = tls . NewListener ( listener , c )
2024-07-08 21:08:00 +00:00
logger . Info ( "Web server running HTTPS on" , listener . Addr ( ) )
2024-02-25 16:07:03 +00:00
} else {
2024-07-08 21:08:00 +00:00
logger . Error ( "Error loading certificates:" , err )
logger . Info ( "Web server running HTTP on" , listener . Addr ( ) )
2023-02-09 19:18:06 +00:00
}
} else {
2024-07-08 21:08:00 +00:00
logger . Info ( "Web server running HTTP on" , listener . Addr ( ) )
2023-02-09 19:18:06 +00:00
}
s . listener = listener
s . httpServer = & http . Server {
Handler : engine ,
}
go func ( ) {
s . httpServer . Serve ( listener )
} ( )
2024-05-09 17:45:12 +00:00
s . startTask ( )
2024-07-07 09:55:59 +00:00
isTgbotenabled , err := s . settingService . GetTgbotEnabled ( )
2023-03-17 16:07:49 +00:00
if ( err == nil ) && ( isTgbotenabled ) {
tgBot := s . tgbotService . NewTgbot ( )
2023-05-20 15:38:01 +00:00
tgBot . Start ( i18nFS )
2023-03-17 16:07:49 +00:00
}
2023-02-09 19:18:06 +00:00
return nil
}
2025-09-20 07:35:50 +00:00
// Stop gracefully shuts down the web server, stops Xray, cron jobs, and Telegram bot.
2023-02-09 19:18:06 +00:00
func ( s * Server ) Stop ( ) error {
s . cancel ( )
s . xrayService . StopXray ( )
if s . cron != nil {
s . cron . Stop ( )
}
2023-05-20 15:09:01 +00:00
if s . tgbotService . IsRunning ( ) {
2023-03-17 16:07:49 +00:00
s . tgbotService . Stop ( )
}
2026-01-03 04:26:00 +00:00
// Gracefully stop WebSocket hub
if s . wsHub != nil {
s . wsHub . Stop ( )
}
2023-02-09 19:18:06 +00:00
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 07:35:50 +00:00
// GetCtx returns the server's context for cancellation and deadline management.
2023-02-09 19:18:06 +00:00
func ( s * Server ) GetCtx ( ) context . Context {
return s . ctx
}
2025-09-20 07:35:50 +00:00
// GetCron returns the server's cron scheduler instance.
2023-02-09 19:18:06 +00:00
func ( s * Server ) GetCron ( ) * cron . Cron {
return s . cron
}
2026-01-03 04:26:00 +00:00
// GetWSHub returns the WebSocket hub instance.
2026-01-05 04:54:56 +00:00
func ( s * Server ) GetWSHub ( ) any {
2026-01-03 04:26:00 +00:00
return s . wsHub
}
2026-01-11 21:57:04 +00:00
// InitRedisCache initializes Redis cache. If redisAddr is empty, uses embedded Redis.
func InitRedisCache ( redisAddr string ) error {
return cache . InitRedis ( redisAddr )
}
// CloseRedisCache closes Redis cache connection.
func CloseRedisCache ( ) error {
return cache . Close ( )
}