Files

330 lines
7.9 KiB
Go
Raw Permalink Normal View History

2025-09-20 09:35:50 +02:00
// Package sub provides subscription server functionality for the 3x-ui panel,
// including HTTP/HTTPS servers for serving subscription links and JSON configurations.
2023-05-22 18:06:34 +03:30
package sub
import (
"context"
"crypto/tls"
"io"
2025-09-14 20:16:40 +02:00
"io/fs"
2023-05-22 18:06:34 +03:30
"net"
"net/http"
2025-09-14 01:22:42 +02:00
"os"
2023-05-22 18:06:34 +03:30
"strconv"
2025-09-16 14:38:18 +02:00
"strings"
"time"
2024-03-11 01:01:24 +03:30
2026-05-10 02:13:42 +02:00
"github.com/mhsanaei/3x-ui/v3/logger"
"github.com/mhsanaei/3x-ui/v3/util/common"
"github.com/mhsanaei/3x-ui/v3/web/locale"
"github.com/mhsanaei/3x-ui/v3/web/middleware"
"github.com/mhsanaei/3x-ui/v3/web/network"
"github.com/mhsanaei/3x-ui/v3/web/service"
2023-05-22 18:06:34 +03:30
"github.com/gin-gonic/gin"
)
2025-09-20 09:35:50 +02:00
// Server represents the subscription server that serves subscription links and JSON configurations.
2023-05-22 18:06:34 +03:30
type Server struct {
httpServer *http.Server
listener net.Listener
sub *SUBController
settingService service.SettingService
ctx context.Context
cancel context.CancelFunc
}
2025-09-20 09:35:50 +02:00
// NewServer creates a new subscription server instance with a cancellable context.
2023-05-22 18:06:34 +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
// initRouter configures the subscription server's Gin engine, middleware,
// templates and static assets and returns the ready-to-use engine.
2023-05-22 18:06:34 +03:30
func (s *Server) initRouter() (*gin.Engine, error) {
2025-09-14 20:16:40 +02:00
// Always run in release mode for the subscription server
gin.DefaultWriter = io.Discard
gin.DefaultErrorWriter = io.Discard
gin.SetMode(gin.ReleaseMode)
2023-05-22 18:06:34 +03:30
engine := gin.Default()
2024-02-21 14:17:52 +03:30
subDomain, err := s.settingService.GetSubDomain()
2023-05-22 18:06:34 +03:30
if err != nil {
return nil, err
}
2024-02-21 14:17:52 +03:30
if subDomain != "" {
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
}
LinksPath, err := s.settingService.GetSubPath()
2023-05-22 18:06:34 +03:30
if err != nil {
return nil, err
}
2024-02-21 14:17:52 +03:30
JsonPath, err := s.settingService.GetSubJsonPath()
if err != nil {
return nil, err
}
2026-04-20 04:26:13 +08:00
ClashPath, err := s.settingService.GetSubClashPath()
if err != nil {
return nil, err
}
2025-09-18 13:56:04 +02:00
subJsonEnable, err := s.settingService.GetSubJsonEnable()
if err != nil {
return nil, err
}
2026-04-20 04:26:13 +08:00
subClashEnable, err := s.settingService.GetSubClashEnable()
if err != nil {
return nil, err
}
2025-09-16 14:38:18 +02:00
// Set base_path based on LinksPath for template rendering
2025-09-24 19:51:01 +02:00
// Ensure LinksPath ends with "/" for proper asset URL generation
basePath := LinksPath
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
basePath += "/"
}
2025-10-09 18:39:29 +03:00
// logger.Debug("sub: Setting base_path to:", basePath)
2025-09-16 14:38:18 +02:00
engine.Use(func(c *gin.Context) {
2025-09-24 19:51:01 +02:00
c.Set("base_path", basePath)
2025-09-16 14:38:18 +02:00
})
2024-02-21 14:17:52 +03:30
Encrypt, err := s.settingService.GetSubEncrypt()
if err != nil {
return nil, err
}
ShowInfo, err := s.settingService.GetSubShowInfo()
if err != nil {
return nil, err
}
RemarkModel, err := s.settingService.GetRemarkModel()
if err != nil {
RemarkModel = "-ieo"
}
SubUpdates, err := s.settingService.GetSubUpdates()
if err != nil {
SubUpdates = "10"
}
SubJsonFragment, err := s.settingService.GetSubJsonFragment()
if err != nil {
SubJsonFragment = ""
2023-05-22 18:06:34 +03:30
}
2024-09-17 09:33:44 +02:00
SubJsonNoises, err := s.settingService.GetSubJsonNoises()
2024-08-29 11:27:43 +02:00
if err != nil {
2024-09-17 09:33:44 +02:00
SubJsonNoises = ""
2024-08-29 11:27:43 +02:00
}
2024-03-12 19:44:51 +03:30
SubJsonMux, err := s.settingService.GetSubJsonMux()
if err != nil {
SubJsonMux = ""
}
SubJsonRules, err := s.settingService.GetSubJsonRules()
if err != nil {
SubJsonRules = ""
}
SubTitle, err := s.settingService.GetSubTitle()
if err != nil {
SubTitle = ""
}
SubSupportUrl, err := s.settingService.GetSubSupportUrl()
if err != nil {
SubSupportUrl = ""
}
SubProfileUrl, err := s.settingService.GetSubProfileUrl()
if err != nil {
SubProfileUrl = ""
}
SubAnnounce, err := s.settingService.GetSubAnnounce()
if err != nil {
SubAnnounce = ""
}
SubEnableRouting, err := s.settingService.GetSubEnableRouting()
if err != nil {
return nil, err
}
SubRoutingRules, err := s.settingService.GetSubRoutingRules()
if err != nil {
SubRoutingRules = ""
}
2025-09-14 01:22:42 +02:00
// set per-request localizer from headers/cookies
engine.Use(locale.LocalizerMiddleware())
2026-05-09 17:38:48 +02:00
// Mount the Vite-built dist/assets/ so the subscription page's JS/CSS
// bundles load from `/assets/...`. Also mount the same FS under the
// subscription path prefix (LinksPath + "assets") so reverse proxies
// running the panel under a URI prefix can resolve those URLs too.
2025-09-16 14:38:18 +02:00
// Note: LinksPath always starts and ends with "/" (validated in settings).
var linksPathForAssets string
if LinksPath == "/" {
linksPathForAssets = "/assets"
} else {
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
}
2025-09-24 19:51:01 +02:00
var assetsFS http.FileSystem
2026-05-09 17:38:48 +02:00
if _, err := os.Stat("web/dist/assets"); err == nil {
assetsFS = http.FS(os.DirFS("web/dist/assets"))
} else if subFS, err := fs.Sub(distFS, "dist/assets"); err == nil {
2026-05-09 17:38:48 +02:00
assetsFS = http.FS(subFS)
2025-09-14 20:16:40 +02:00
} else {
2026-05-09 17:38:48 +02:00
logger.Error("sub: failed to mount embedded dist assets:", err)
2025-09-14 20:16:40 +02:00
}
2025-09-14 01:22:42 +02:00
2025-09-24 19:51:01 +02:00
if assetsFS != nil {
engine.StaticFS("/assets", assetsFS)
if linksPathForAssets != "/assets" {
engine.StaticFS(linksPathForAssets, assetsFS)
}
2026-05-09 17:38:48 +02:00
// Browser may resolve subpage assets relative to the request URL —
// /sub/<basePath>/<subId>/assets/... — so route those to the same FS.
2025-09-24 19:51:01 +02:00
if LinksPath != "/" {
engine.Use(func(c *gin.Context) {
path := c.Request.URL.Path
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
assetsIndex := strings.Index(path, "/assets/")
if assetsIndex != -1 {
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
if assetPath != "" {
c.FileFromFS(assetPath, assetsFS)
c.Abort()
return
}
}
}
c.Next()
})
}
}
2024-02-21 14:17:52 +03:30
g := engine.Group("/")
2023-05-22 18:06:34 +03:30
2024-03-12 19:44:51 +03:30
s.sub = NewSUBController(
2026-04-20 04:26:13 +08:00
g, LinksPath, JsonPath, ClashPath, subJsonEnable, subClashEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
2023-05-22 18:06:34 +03:30
return engine, nil
}
2025-09-20 09:35:50 +02:00
// Start initializes and starts the subscription server with configured settings.
2023-05-22 18:06:34 +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-05-22 18:06:34 +03:30
defer func() {
if err != nil {
s.Stop()
}
}()
subEnable, err := s.settingService.GetSubEnable()
if err != nil {
return err
}
if !subEnable {
return nil
}
engine, err := s.initRouter()
if err != nil {
return err
}
certFile, err := s.settingService.GetSubCertFile()
if err != nil {
return err
}
keyFile, err := s.settingService.GetSubKeyFile()
if err != nil {
return err
}
listen, err := s.settingService.GetSubListen()
if err != nil {
return err
}
port, err := s.settingService.GetSubPort()
if err != nil {
return err
}
2023-05-31 01:24:18 +04:30
2023-05-22 18:06:34 +03:30
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
return err
}
2023-05-31 01:24:18 +04:30
2023-05-22 18:06:34 +03:30
if certFile != "" || keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
2024-03-11 11:37:16 +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("Sub server running HTTPS on", listener.Addr())
2024-03-11 11:37:16 +03:30
} else {
2024-07-08 23:08:00 +02:00
logger.Error("Error loading certificates:", err)
logger.Info("Sub server running HTTP on", listener.Addr())
2023-05-22 18:06:34 +03:30
}
} else {
2024-07-08 23:08:00 +02:00
logger.Info("Sub server running HTTP on", listener.Addr())
2023-05-22 18:06:34 +03:30
}
s.listener = listener
s.httpServer = &http.Server{
Handler: engine,
}
go func() {
s.httpServer.Serve(listener)
}()
return nil
}
2025-09-20 09:35:50 +02:00
// Stop gracefully shuts down the subscription server and closes the listener.
2023-05-22 18:06:34 +03:30
func (s *Server) Stop() error {
s.cancel()
var err1 error
var err2 error
if s.httpServer != nil {
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
err1 = s.httpServer.Shutdown(shutdownCtx)
2023-05-22 18:06:34 +03:30
}
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-05-22 18:06:34 +03:30
func (s *Server) GetCtx() context.Context {
return s.ctx
2023-05-31 01:24:18 +04:30
}